App-FargateStack
view release on metacpan or search on metacpan
lib/App/FargateStack/Checker.pm view on Meta::CPAN
if (@missing_adv) {
return row_warn( 'Service-linked roles', 'Advisory missing: ' . join ', ', @missing_adv );
}
return row_ok( 'Service-linked roles', 'Present or auto-creatable' );
}
########################################################################
sub check_vpc_and_subnets {
########################################################################
my ($opt) = @_;
my $ec2 = new_client( 'App::EC2', $opt );
my ( $ok_vpc, $vpcs, $err_vpc ) = try_aws(
sub {
$ec2->command( 'describe-vpcs' => [ '--query' => 'Vpcs[].VpcId' ] );
}
);
if ( !$ok_vpc || !$vpcs || !@{$vpcs} ) {
return row_fail( 'VPC/Subnets', 'No VPCs accessible in this region' );
}
my ( $ok_sub, $subs, $err_sub ) = try_aws(
sub {
$ec2->command( 'describe-subnets' => [ '--query' => 'Subnets[].AvailabilityZone' ] );
}
);
if ( !$ok_sub || !$subs || !@{$subs} ) {
return row_fail( 'VPC/Subnets', 'No subnets found' );
}
my %az = map { $_ => 1 } @{$subs};
my $az_count = scalar keys %az;
if ( $az_count < 2 ) {
return row_fail( 'VPC/Subnets', 'Need at least two subnets across different AZs' );
}
return row_ok( 'VPC/Subnets', sprintf 'OK (%d AZs)', $az_count );
}
########################################################################
sub check_egress {
########################################################################
my ( $opt, $need_secrets ) = @_;
my $ec2 = new_client( 'App::EC2', $opt );
my ( $ok_nat, $nat, $err_nat ) = try_aws(
sub {
$ec2->command(
'describe-nat-gateways' => [ '--filter', 'Name=state,Values=available', '--query', 'NatGateways[].NatGatewayId' ] );
}
);
my ( $ok_ep, $eps, $err_ep ) = try_aws(
sub {
$ec2->command( 'describe-vpc-endpoints' => [ '--query', 'VpcEndpoints[].ServiceName' ] );
}
);
my $has_nat = $ok_nat && $nat && @{$nat};
my $has_eps = $ok_ep && $eps;
if ( !$has_nat && !$has_eps ) {
return row_fail( 'Egress', 'Private subnets need NAT or VPC endpoints (ECR, Logs, Secrets)' );
}
my %need = (
'com.amazonaws.%s.ecr.api' => 1,
'com.amazonaws.%s.ecr.dkr' => 1,
'com.amazonaws.%s.logs' => 1,
);
if ($need_secrets) {
$need{'com.amazonaws.%s.secretsmanager'} = 1;
}
if ($has_eps) {
my %have = map { $_ => 1 } @{$eps};
my @missing;
foreach my $tmpl ( keys %need ) {
my $svc = sprintf $tmpl, ( $opt->{region} || 'us-east-1' );
if ( !$have{$svc} ) {
push @missing, $svc;
}
}
if ( @missing && !$has_nat ) {
return row_fail( 'Egress', 'Missing VPC endpoints: ' . join q{, }, @missing );
}
if ( @missing && $has_nat ) {
return row_ok( 'Egress', 'Using NAT; missing optional endpoints: ' . join q{, }, @missing );
}
}
return row_ok( 'Egress', $has_nat ? 'NAT available' : 'Required endpoints present' );
}
########################################################################
sub check_ecs_permissions {
########################################################################
my ($opt) = @_;
my $ecs = new_client( 'App::ECS', $opt );
my ( $ok1, $clusters, $err1 ) = try_aws( sub { $ecs->command( 'list-clusters' => [ '--query' => 'clusterArns[]' ] ) } );
my ( $ok2, $tds, $err2 ) = try_aws( sub { $ecs->command( 'list-task-definitions' => [ '--max-results' => '1' ] ) } );
if ( !$ok1 || !$ok2 ) {
return row_fail( 'ECS perms', 'Missing list/describe permissions' );
}
return row_ok( 'ECS perms', 'List/Describe OK (create/update assumed during deploy)' );
}
########################################################################
sub check_elbv2_permissions {
########################################################################
my ($opt) = @_;
my $elbv2 = new_client( 'App::ElbV2', $opt );
my ( $ok, $lbs, $err )
= try_aws( sub { $elbv2->command( 'describe-load-balancers' => [ '--query' => 'LoadBalancers[].LoadBalancerArn' ] ) } );
if ( !$ok ) {
return row_fail( 'ELBv2 perms', 'Cannot describe load balancers' );
}
return row_ok( 'ELBv2 perms', 'Describe OK' );
}
########################################################################
sub check_logs_permissions {
########################################################################
my ($opt) = @_;
my $logs = new_client( 'App::Logs', $opt );
my ( $ok, $lgs, $err ) = try_aws( sub { $logs->command( 'describe-log-groups' => [ '--limit' => '1' ] ) } );
if ( !$ok ) {
return row_fail( 'CloudWatch Logs perms', 'Cannot describe log groups' );
}
return row_ok( 'CloudWatch Logs perms', 'Describe OK' );
}
########################################################################
sub check_route53 {
########################################################################
my ($self) = @_;
my %options = (
profile => $self->get_route53_profile,
region => $self->get_region,
log_level => $self->get_log_level,
);
my $r53 = new_client( 'App::Route53' => %options );
my ( $ok, $hzs, $err ) = try_aws( sub { $r53->command( 'list-hosted-zones' => [ '--query' => 'HostedZones[].Id' ] ) } );
if ( !$ok ) {
return row_fail( 'Route 53', 'Cannot list hosted zones' );
}
return row_ok( 'Route 53' => 'List OK' );
}
########################################################################
sub check_acm {
########################################################################
my ($opt) = @_;
my $acm = new_client( 'App::ACM', $opt );
my ( $ok, $certs, $err )
= try_aws( sub { $acm->command( 'list-certificates' => [ '--query' => 'CertificateSummaryList[].CertificateArn' ] ) } );
if ( !$ok ) {
return row_fail( 'ACM', 'Cannot list certificates in region' );
}
return row_ok( ACM => 'List OK' );
}
########################################################################
sub check_ecr_access {
########################################################################
my ($opt) = @_;
my $ecr = new_client( 'App::ECR', $opt );
my ( $ok, $repos, $err ) = try_aws( sub { $ecr->command( 'describe-repositories' => [ '--max-results' => '1' ] ) } );
if ( !$ok ) {
return row_warn( 'ECR', 'Cannot describe repositories; image validation may fail' );
}
return row_ok( 'ECR', 'Describe OK' );
}
########################################################################
sub check_secrets_hint {
########################################################################
my ($opt) = @_;
# The deployer does not need GetSecretValue; tasks use task role.
# We only check control-plane reachability here.
my $ec2 = new_client( 'App::EC2', $opt );
my ( $ok_ep, $eps, $err_ep ) = try_aws(
sub {
$ec2->command( 'describe-vpc-endpoints' => [ '--query', 'VpcEndpoints[].ServiceName' ] );
}
);
if ( !$ok_ep ) {
return row_warn( 'Secrets Manager', 'Could not confirm VPC endpoints for Secrets Manager' );
}
my %have = map { $_ => 1 } @{ $eps || [] };
my $svc = sprintf 'com.amazonaws.%s.secretsmanager', ( $opt->{region} || 'us-east-1' );
if ( !$have{$svc} ) {
return row_warn( 'Secrets Manager', 'Missing VPC endpoint for Secrets Manager or rely on NAT' );
}
return row_ok( 'Secrets Manager', 'Endpoint present' );
}
########################################################################
sub check_passrole {
########################################################################
my ($opt) = @_;
my $role_names = $opt->get_role_names;
# role_names: arrayref of the exact role names your framework will create,
# e.g., [ 'FargateStack/my-svc/TaskExecutionRole', 'FargateStack/my-svc/TaskRole',
# 'FargateStack/my-svc/EventsInvokeRole' ]
my $sts = new_client( 'App::STS', $opt );
my $iam = new_client( 'App::IAM', $opt );
my $id = $sts->get_caller_identity();
my $acct = $id->{Account} || q{};
my $caller_arn = $id->{Arn} || q{};
if ( !$acct || !$caller_arn ) {
return row_fail( 'iam:PassRole', 'Cannot resolve caller identity' );
}
my $policy_source_arn = _principal_to_policy_source_arn( $caller_arn, $acct );
if ( !$policy_source_arn ) {
return row_fail( 'iam:PassRole', 'Cannot derive policy-source ARN for simulation' );
}
my $role_arns = _role_arns_from_names( $acct, $role_names || [] );
if ( !$role_arns || !@{$role_arns} ) {
return row_warn( 'iam:PassRole', 'No target roles specified; skipping simulation' );
}
my @services = qw(ecs-tasks.amazonaws.com events.amazonaws.com);
my %decisions_by_service;
foreach my $svc (@services) {
my $result = eval { _simulate_passrole_once( $iam, $policy_source_arn, $role_arns, $svc ) };
if ( $EVAL_ERROR || !$result ) {
# If org blocks simulation, warn rather than fail
return row_warn( 'iam:PassRole', 'Simulation not permitted; verify PassRole manually' );
}
# result is an arrayref of EvalDecision strings: allowed | explicitDeny | implicitDeny
my @dec = @{ $result || [] };
$decisions_by_service{$svc} = \@dec;
}
# Aggregate decisions: any explicitDeny -> FAIL
# otherwise any implicitDeny -> WARN
# otherwise all allowed -> PASS
my $detail = q{};
my $status = 'PASS';
foreach my $svc (@services) {
my $dec = $decisions_by_service{$svc} || [];
my $svc_summary = sprintf '%s: %s', $svc, ( join q{,}, @{$dec} );
lib/App/FargateStack/Checker.pm view on Meta::CPAN
__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:
YES = all required checks PASS
MAYBE = at least one required check WARN (no FAILs)
NO = at least one required check FAIL or a required check is missing
The gates for each capability are:
HTTP services : Credentials, VPC/Subnets, Egress, ECS perms,
ELBv2 perms, CloudWatch Logs perms, iam:PassRole
HTTPS service : HTTP services + 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
=head2 EXIT STATUS
0 All checks PASS
1 One or more checks WARN (no FAILs)
2 One or more checks FAIL
These exit codes are designed for CI gating.
=head2 NOTES
=over 4
=item * Read-only
The checker performs only C<Describe*/List*> calls and IAM simulation where
available. It does not create, update, or delete resources.
=item * Service-linked roles (SLRs)
The checker reports on SLRs relevant to ECS/ELB. EventBridge does not have a
single generic âAWSServiceRoleForEventsâ; feature-specific SLRs are outside the
default scope.
=item * Egress semantics
For private subnets, NAT is sufficient for pulling images and writing logs.
VPC endpoints for ECR (api+dkr) and Logs are recommended in restricted/air-gapped
environments; missing endpoints are reported as optional when NAT is present.
=item * iam:PassRole simulation
When role names are known or derivable, the checker simulates C<iam:PassRole>
for each target role against C<ecs-tasks.amazonaws.com> and
C<events.amazonaws.com>. If role names are not available, the tool may WARN and
capabilities depending on PassRole may degrade to MAYBE.
=item * Separate DNS account
If Route 53 is managed in another account, use C<--dns-profile>. The header will
display the active âRoute53 profileâ for clarity.
=back
=head1 EXAMPLES
Minimal, default profile/region:
app-FargateStack-env.pl
Explicit profile/region:
app-FargateStack-env.pl --profile sandbox --region us-east-1
Skip DNS checks entirely:
app-FargateStack-env.pl --no-dns
Use a separate Route 53 account, and enable HTTPS + Secrets checks:
app-FargateStack-env.pl --profile sandbox --region us-east-1 \
--dns-profile prod --https --secrets
=head1 SEE ALSO
ECS, Fargate, ELBv2, EventBridge, Route 53, ACM, CloudWatch Logs, ECR.
=cut
( run in 0.681 second using v1.01-cache-2.11-cpan-39bf76dae61 )