Validations to Enforce Limits on Security Group Rules

Overview

This example shows you how to write a validation module in Ludwig to ensure a composition doesn’t exceed a certain number of security group rules. The validation can be used as a design-time validation or a runtime validation.

Prerequisites

You’ll need to have the Fugue Client Tools installed. If you plan to use the validation at runtime, you’ll also need to install the Conductor. If you haven’t done so yet, it just takes a few quick steps.

What We’ll Do In This Example

We’ll cover how to write Ludwig to make a validation module that enforces certain rules governing your infrastructure. We’ll also demonstrate how a compliant composition passes validation and a noncompliant composition fails.

What We’ll Have When We’re Done

A module that can be used as a design-time validation or a runtime validation (or both) to limit the number of ingress or egress rules for each security group in a composition.

How Long It Will Take

About 20 minutes.

Download

You can download the source code for this example here:

If you executed init during the Quick Setup, download the compositions to the directory where you ran the command.

Get editor plug-ins here.

Let’s Go!

AWS has a soft limit of 50 ingress rules and 50 egress rules on a single security group, so let’s construct a validation module that checks each security group in a composition and throws an error if there are more than 50 inbound or outbound rules on it.

Importing a Library

Our first order of business is writing the validation module, which we’ll call SgValidation.lw. We’ll begin by importing the Fugue.AWS.EC2 library. This brings the EC2 types we need into scope.

# This validation module counts the ingress and egress rules on a security
# group and throws an error if either exceeds 50.

import Fugue.AWS.EC2 as EC2

Writing the Validation Functions

Both validation functions work the same way: Add a composition’s security group rules to a list, then check the list’s length. If it exceeds 50 items, throw an error. The difference is that one function adds the egress rules to a list, and the other function adds the ingress rules to a list.

We’ll start with the egress rule validation function.

Egress Rule Validation

We’ve named this function noMoreThan50EgressRules. It takes an EC2.SecurityGroup and returns a Validation.

# If a security group has an ipPermissionsEgress field,
# egressRules = ipPermissionsEgress. If it doesn't, egressRules = empty list.
# Check length of egressRules and throw an error if it exceeds 50.
fun noMoreThan50EgressRules(sg: EC2.SecurityGroup) -> Validation:
  let egressRules: sg.(EC2.SecurityGroup).ipPermissionsEgress ?| []
  if List.length(egressRules) > 50 then
    Validation.error {message: "You cannot have more than 50 egress rules in a Security Group"}
  else
    Validation.success
Let Statement

In the function body, we declare a local variable called egressRules. Let’s look at that line again:

let egressRules: sg.(EC2.SecurityGroup).ipPermissionsEgress ?| []

In order to count how many egress rules are attached to a security group, we need to retrieve the value of ipPermissionsEgress. So, we use dot notation to indicate that in the parameter sg (which is of the EC2.SecurityGroup type), we want to drill down to the EC2.SecurityGroup constructor and return the ipPermissionsEgress field.

As you can see from the EC2.SecurityGroup.new constructor, ipPermissionsEgress is an optional list of IP permissions (Optional<List<IpPermission>>).

An optional field means there are two states: either the field is present in the composition, or it isn’t. The Optional.unpack function (or its operator, ?|) “unpacks” a value, which means it accounts for both options:

  • If that field is present, return its value.
  • If that field is not present, return a “default” response.

Here, we’re using the Optional.unpack operator, ?|, to return the value of the ipPermissionsEgress field:

  • If the ipPermissionsEgress field is present in the security group, return that value.
  • If the ipPermissionsEgress field is not present, return the default response, which is an empty list ([]).

The ?| operator is convenient shorthand for the Optional.unpack function, so we’ve written this validation function with the operator. But if we were to write it with the Optional.unpack function, it would look like this and achieve the same results:

let egressRules: Optional.unpack([], sg.(EC2.SecurityGroup).ipPermissionsEgress)

Note that the argument positions are reversed. With Optional.unpack, the default argument comes first. With ?|, the default argument comes second.

The end result is that if a security group has egress rules, assign the list of rules to egressRules. If the security group doesn’t have any egress rules, assign an empty list ([]) to egressRules.

If/Then Statement

The second half of the validation function uses an if/then statement to define failure and success conditions.

if List.length(egressRules) > 50 then
  Validation.error {message: "You cannot have more than 50 egress rules in a Security Group"}
else
  Validation.success

Recall that egressRules is either a list of security group egress rules or an empty list. The List.length function simply counts the number of items in a list. So, if there are more than 50 items in egressRules, the Validation.error function returns an error message. Otherwise, Validation.success returns a “success” value.

Put the whole function together, and the compiler counts the egress rules on a security group and throws a validation error if there are more than 50.

Ingress Rule Validation

The ingress rule validation, noMoreThan50IngressRules, uses the same logic as the egress rule validation:

# If a security group has an ipPermissions field,
# ingressRules = ipPermissions. If it doesn't, ingressRules = empty list.
# Check length of ingressRules and throw an error if it exceeds 50.
fun noMoreThan50IngressRules(sg: EC2.SecurityGroup) -> Validation:
  let ingressRules: sg.(EC2.SecurityGroup).ipPermissions ?| []
  if List.length(ingressRules) > 50 then
    Validation.error {message: "You cannot have more than 50 ingress rules in a Security Group"}
  else
    Validation.success

The only difference is that the local variable ingressRules represents the value of the ipPermissions field. Once again, the compiler counts the ingress rules on a security group and throws a validation error if there are more than 50.

Registering the Validation Functions

Finally, the validate keyword registers the validation function so that it takes effect on all EC2.SecurityGroup resources in scope. We register both validation functions in these lines:

# Register each validation function.
validate noMoreThan50EgressRules
validate noMoreThan50IngressRules

This ensures that both validations are applied to each EC2.SecurityGroup in a composition.

Note: The validation registration can appear before or after its validation. In this case, we’ve put both at the end of the file.

Applying the Validation Module

Now that we’ve written the validation module, we can put it to work.

As we hinted earlier, there are two types of validations:

For more details on the differences between design-time and runtime validations, see Design-Time vs. Runtime Validations.

You can use whichever method suits your needs (or you can use both).

  • To use this module as a design-time validation, import it into a composition and compile that composition with lwc (further details available here). To test the examples below, uncomment line 9 of each composition.
  • To use this module as a runtime validation, upload it to the Conductor and fugue run a composition (further details available here).

Testing a Compliant Composition

We’ll leave the choice of design-time or runtime (or both) up to you. Either way, you can try out the validation module with the following composition to see how it passes validation – just uncomment line 9 if you are validating at design-time.

# This test composition is designed to pass validation in SgValidation.lw.
# The security group contains no ingress or egress rules.
composition

import Fugue.AWS as AWS
import Fugue.AWS.EC2 as EC2

# Uncomment the line below to test validation locally with `lwc`:
# import SgValidation as SgValidation

vpc: EC2.Vpc.new {
  cidrBlock: "10.0.0.0/16",
  region: AWS.Us-east-1
}

simple-sg: EC2.SecurityGroup.new {
    description: "Empty SG",
    vpc: vpc
}

The SgTestPass.lw composition declares a VPC and empty security group. As you can see, there are zero security group rules, so this composition should pass validation.

Whether you choose to implement SgValidation.lw as a design-time or runtime validation, you’ll see that SgTestPass.lw passes the test. If you’re validating it locally, you’ll see that it compiles successfully, without output. If you’re validating it at runtime, you’ll see that the Conductor successfully launches it as a process.

Testing a Noncompliant Composition

Now, let’s try the validation module with a noncompliant composition. Once again, if you’re validating at design-time, uncomment line 9.

# This test composition is designed to fail validation in SgValidation.lw.
# It creates 51 egress and 51 ingress rules on the security group.
composition

import Fugue.AWS as AWS
import Fugue.AWS.EC2 as EC2

# Uncomment the line below to test validation locally with `lwc`:
# import SgValidation as SgValidation

vpc: EC2.Vpc.new {
  cidrBlock: "10.0.0.0/16",
  region: AWS.Us-east-1
}

simple-sg: EC2.SecurityGroup.new {
    description: "Empty SG",
    vpc: vpc,
    ipPermissions: [EC2.IpPermission.tcp(port, EC2.IpPermission.Target.all) for port in Int.range {from: 1, to: 51}],
    ipPermissionsEgress: [EC2.IpPermission.udp(port, EC2.IpPermission.Target.all) for port in Int.range {from: 1, to: 51}],
}

The SgTestFail.lw composition declares a VPC and a security group with ingress and egress rules.

To provide a value for the security group’s ipPermissions, the EC2.IpPermission.tcp function creates an ingress rule with the protocol TCP and the target all ("0.0.0.0/0") for each given port, which ranges from 1 to 51. The result is a list of ingress rules where one rule has port 1, another has port 2, and so on up through 51. This list is assigned to the ipPermissions field.

The security group’s ipPermissionsEgress value is determined the same way, but with the EC2.IpPermission.udp function, which uses UDP instead of TCP.

In all, this composition declares 51 ingress rules and 51 egress rules on a security group – exceeding the limit of 50 for either – so this composition will not pass validation.

Whether you choose to implement SgValidation.lw as a design-time or runtime validation, you’ll see that SgTestFail.lw fails the test. If you’re validating it locally, you’ll see that compilation is unsuccessful. If you’re validating it at runtime, you’ll see that the Conductor does not run the process. In either case, you should see an error message like this:

"/tmp/349448605/composition/src/SgTestFail.lw" (line 13, column 12):
Validations failed:

  13| simple-sg: EC2.SecurityGroup {
  14|     description: "Empty SG",
  15|     vpc: vpc,
  16|     ipPermissions: List.map(createIngressRule, Int.range{from:1,to:51}),
  17|     ipPermissionsEgress: List.map(createEgressRule, Int.range{from:1,to:51})
  18| }

  - You cannot have more than 50 ingress rules in a Security Group
    (from SgValidation.noMoreThan50IngressRules)
  - You cannot have more than 50 egress rules in a Security Group
    (from SgValidation.noMoreThan50EgressRules)

Note: Output before and after the error message will be different based on whether you’re testing it locally or on the Conductor.

Next Steps

If this example has whetted your appetite for validations, you can learn about testing compositions at compile-time with Design-Time Validations. See Runtime Validations for information on testing compositions at provision-time. NodeStream validations can be applied at design-time or runtime – read about them at Validations Across Types and Values. For more information about how design-time and runtime validations differ, see Design-Time vs. Runtime Validations. If you’d like a refresher on writing Ludwig functions, see Functions Tutorial. And as always, reach out to support@fugue.co with any questions.