Repository: klothoplatform/klotho
Pull request #804 (2023-12-05 21:39:14)

Unique can add
Plus some misc determinism fixes uncovered by the original fix.


Patch:
diff --git a/.github/workflows/govulncheck.yaml b/.github/workflows/govulncheck.yaml
index e5603dd3..654d25ed 100644
--- a/.github/workflows/govulncheck.yaml
+++ b/.github/workflows/govulncheck.yaml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/setup-go@v3
         with:
-          go-version: '1.21.4'
+          go-version: '1.21.5'
       - uses: actions/cache@v2
         with:
           path: |
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index fceaec9a..625c3d57 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/setup-go@v3
         with:
-          go-version: '1.21.4'
+          go-version: '1.21.5'
       - uses: actions/checkout@v3
       - uses: actions/cache@v2
         with:
diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml
index 8862a528..23e9660e 100644
--- a/.github/workflows/prettier.yaml
+++ b/.github/workflows/prettier.yaml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/setup-go@v3
         with:
-          go-version: '1.21.4'
+          go-version: '1.21.5'
       - uses: actions/checkout@v3
       - name: List files to check
         run: |
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index f092a17c..95a120a7 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -25,7 +25,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.21.4'
+          go-version: '1.21.5'
 
       - name: Setup Zig
         uses: goto-bus-stop/setup-zig@v1
diff --git a/pkg/construct2/graph.go b/pkg/construct2/graph.go
index 2581bf01..8e9a0876 100644
--- a/pkg/construct2/graph.go
+++ b/pkg/construct2/graph.go
@@ -68,7 +68,7 @@ func stringTo(g Graph, w io.Writer) error {
 		for t := range adjacent[id] {
 			targets = append(targets, t)
 		}
-		sort.Sort(sortedIds(targets))
+		sort.Sort(SortedIds(targets))
 
 		for _, t := range targets {
 			e := adjacent[id][t]
diff --git a/pkg/construct2/graph_deps.go b/pkg/construct2/graph_deps.go
index 2b3a432e..27ca9e2a 100644
--- a/pkg/construct2/graph_deps.go
+++ b/pkg/construct2/graph_deps.go
@@ -31,7 +31,7 @@ func DirectDownstreamDependencies(g Graph, r ResourceId) ([]ResourceId, error) {
 			ids = append(ids, e.Target)
 		}
 	}
-	sort.Sort(sortedIds(ids))
+	sort.Sort(SortedIds(ids))
 
 	return ids, nil
 }
@@ -61,7 +61,7 @@ func DirectUpstreamDependencies(g Graph, r ResourceId) ([]ResourceId, error) {
 			ids = append(ids, e.Source)
 		}
 	}
-	sort.Sort(sortedIds(ids))
+	sort.Sort(SortedIds(ids))
 
 	return ids, nil
 }
@@ -73,7 +73,7 @@ func allDependencies(deps map[ResourceId]map[ResourceId]Edge, r ResourceId) []Re
 	for d := range deps[r] {
 		stack = append(stack, d)
 	}
-	sort.Sort(sortedIds(stack))
+	sort.Sort(SortedIds(stack))
 
 	var ids []ResourceId
 	for len(stack) > 0 {
@@ -90,7 +90,7 @@ func allDependencies(deps map[ResourceId]map[ResourceId]Edge, r ResourceId) []Re
 			}
 			next = append(next, d)
 		}
-		sort.Sort(sortedIds(next))
+		sort.Sort(SortedIds(next))
 		stack = append(stack, next...)
 	}
 
diff --git a/pkg/construct2/graph_io.go b/pkg/construct2/graph_io.go
index 93c76cdf..0b8b90e5 100644
--- a/pkg/construct2/graph_io.go
+++ b/pkg/construct2/graph_io.go
@@ -73,7 +73,7 @@ func (g YamlGraph) MarshalYAML() (interface{}, error) {
 		for t := range adj[source] {
 			targets = append(targets, t)
 		}
-		sort.Sort(sortedIds(targets))
+		sort.Sort(SortedIds(targets))
 		for _, target := range targets {
 			edges.Content = append(edges.Content,
 				&yaml.Node{
diff --git a/pkg/construct2/graph_vertices.go b/pkg/construct2/graph_vertices.go
index 05546309..c90d4797 100644
--- a/pkg/construct2/graph_vertices.go
+++ b/pkg/construct2/graph_vertices.go
@@ -6,35 +6,6 @@ import (
 	"sort"
 )
 
-// sortedIds is a helper type for sorting ResourceIds by purely their content, for use when deterministic ordering
-// is desired (when no other sources of ordering are available).
-type sortedIds []ResourceId
-
-func (s sortedIds) Len() int {
-	return len(s)
-}
-
-func ResourceIdLess(a, b ResourceId) bool {
-	if a.Provider != b.Provider {
-		return a.Provider < b.Provider
-	}
-	if a.Type != b.Type {
-		return a.Type < b.Type
-	}
-	if a.Namespace != b.Namespace {
-		return a.Namespace < b.Namespace
-	}
-	return a.Name < b.Name
-}
-
-func (s sortedIds) Less(i, j int) bool {
-	return ResourceIdLess(s[i], s[j])
-}
-
-func (s sortedIds) Swap(i, j int) {
-	s[i], s[j] = s[j], s[i]
-}
-
 // ToplogicalSort provides a stable topological ordering of resource IDs.
 // This is a modified implementation of graph.StableTopologicalSort with the primary difference
 // being any uses of the internal function `enqueueArbitrary`.
@@ -92,9 +63,9 @@ func toplogicalSort(g Graph, invertLess bool) ([]ResourceId, error) {
 
 			// Tie-break on the ID contents themselves
 			if invertLess {
-				return !sortedIds(remainingIds).Less(i, j)
+				return !SortedIds(remainingIds).Less(i, j)
 			}
-			return sortedIds(remainingIds).Less(i, j)
+			return SortedIds(remainingIds).Less(i, j)
 		})
 		enqueue(remainingIds[0])
 	}
@@ -106,7 +77,7 @@ func toplogicalSort(g Graph, invertLess bool) ([]ResourceId, error) {
 	order := make([]ResourceId, 0, len(predecessorMap))
 	visited := make(map[ResourceId]struct{})
 
-	sort.Sort(sortedIds(queue))
+	sort.Sort(SortedIds(queue))
 
 	for len(queue) > 0 {
 		currentVertex := queue[0]
@@ -137,9 +108,9 @@ func toplogicalSort(g Graph, invertLess bool) ([]ResourceId, error) {
 		}
 
 		if invertLess {
-			sort.Sort(sort.Reverse(sortedIds(frontier)))
+			sort.Sort(sort.Reverse(SortedIds(frontier)))
 		} else {
-			sort.Sort(sortedIds(frontier))
+			sort.Sort(SortedIds(frontier))
 		}
 
 		enqueue(frontier...)
diff --git a/pkg/construct2/id_sort.go b/pkg/construct2/id_sort.go
new file mode 100644
index 00000000..5cd509a7
--- /dev/null
+++ b/pkg/construct2/id_sort.go
@@ -0,0 +1,30 @@
+package construct2
+
+// SortedIds is a helper type for sorting ResourceIds by purely their content, for use when deterministic ordering
+// is desired (when no other sources of ordering are available).
+type SortedIds []ResourceId
+
+func (s SortedIds) Len() int {
+	return len(s)
+}
+
+func ResourceIdLess(a, b ResourceId) bool {
+	if a.Provider != b.Provider {
+		return a.Provider < b.Provider
+	}
+	if a.Type != b.Type {
+		return a.Type < b.Type
+	}
+	if a.Namespace != b.Namespace {
+		return a.Namespace < b.Namespace
+	}
+	return a.Name < b.Name
+}
+
+func (s SortedIds) Less(i, j int) bool {
+	return ResourceIdLess(s[i], s[j])
+}
+
+func (s SortedIds) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
diff --git a/pkg/construct2/paths.go b/pkg/construct2/paths.go
index 0e9d8ffc..da26044c 100644
--- a/pkg/construct2/paths.go
+++ b/pkg/construct2/paths.go
@@ -126,7 +126,7 @@ func bellmanFord(g Graph, source ResourceId, skipEdge func(Edge) bool) (*bellman
 	for key := range adjacencyMap {
 		sortedKeys = append(sortedKeys, key)
 	}
-	sort.Sort(sortedIds(sortedKeys))
+	sort.Sort(SortedIds(sortedKeys))
 
 	for i := 0; i < len(adjacencyMap)-1; i++ {
 		for _, key := range sortedKeys {
diff --git a/pkg/engine2/operational_rule/operational_action.go b/pkg/engine2/operational_rule/operational_action.go
index 295798b5..8d6636d2 100644
--- a/pkg/engine2/operational_rule/operational_action.go
+++ b/pkg/engine2/operational_rule/operational_action.go
@@ -3,6 +3,7 @@ package operational_rule
 import (
 	"errors"
 	"fmt"
+	"sort"
 
 	"github.com/dominikbraun/graph"
 	"github.com/klothoplatform/klotho/pkg/collectionutil"
@@ -121,19 +122,11 @@ func (action *operationalResourceAction) useAvailableResources(resource *constru
 	configCtx := solution_context.DynamicCtx(action.ruleCtx.Solution)
 	availableResources := make(set.Set[*construct.Resource])
 
-	var err error
-	var directDeps, oppositeDeps map[construct.ResourceId]map[construct.ResourceId]construct.Edge
-	if action.Step.Direction == knowledgebase.DirectionDownstream {
-		var perr error
-		directDeps, err = action.ruleCtx.Solution.DataflowGraph().AdjacencyMap()
-		oppositeDeps, perr = action.ruleCtx.Solution.DataflowGraph().PredecessorMap()
-		err = errors.Join(err, perr)
-	} else {
-		var perr error
-		directDeps, err = action.ruleCtx.Solution.DataflowGraph().PredecessorMap()
-		oppositeDeps, perr = action.ruleCtx.Solution.DataflowGraph().AdjacencyMap()
-		err = errors.Join(err, perr)
+	edges, err := action.ruleCtx.Solution.DataflowGraph().Edges()
+	if err != nil {
+		return err
 	}
+	resources, err := construct.ToplogicalSort(action.ruleCtx.Solution.RawView())
 	if err != nil {
 		return err
 	}
@@ -159,10 +152,6 @@ func (action *operationalResourceAction) useAvailableResources(resource *constru
 				continue
 			}
 
-			resources, err := construct.ToplogicalSort(action.ruleCtx.Solution.RawView())
-			if err != nil {
-				return err
-			}
 			for _, resId := range resources {
 				res, err := action.ruleCtx.Solution.RawView().Vertex(resId)
 				if err != nil {
@@ -230,46 +219,7 @@ func (action *operationalResourceAction) useAvailableResources(resource *constru
 				if edgeTmpl == nil {
 					continue
 				}
-
-				if !edgeTmpl.Unique.Target || !edgeTmpl.Unique.Source {
-					// many-to-many is okay
-					availableResources.Add(res)
-					continue
-				}
-				switch action.Step.Direction {
-				case knowledgebase.DirectionDownstream:
-					if !edgeTmpl.Unique.Source {
-						// many-to-one is okay
-						availableResources.Add(res)
-						continue
-					}
-
-				case knowledgebase.DirectionUpstream:
-					if !edgeTmpl.Unique.Target {
-						// one-to-many are okay
-						availableResources.Add(res)
-						continue
-					}
-				}
-
-				// is unique, check to see if we already have one
-				// this works for both up/downstream because `directDeps` is the correct direction
-				hasType := false
-				for d := range directDeps[resource.ID] {
-					if d.QualifiedTypeName() == res.ID.QualifiedTypeName() {
-						hasType = true
-						break
-					}
-				}
-				if !hasType {
-					for d := range oppositeDeps[res.ID] {
-						if d.QualifiedTypeName() == resource.ID.QualifiedTypeName() {
-							hasType = true
-							break
-						}
-					}
-				}
-				if !hasType {
+				if edgeTmpl.Unique.CanAdd(edges, edge.Source, edge.Target) {
 					availableResources.Add(res)
 				}
 			}
@@ -290,7 +240,11 @@ func (action *operationalResourceAction) placeResources(resource *construct.Reso
 	}
 	placer := placerGen()
 	placer.SetCtx(action.ruleCtx)
-	return placer.PlaceResources(resource, action.Step, availableResources.ToSlice(), &action.numNeeded)
+	resources := availableResources.ToSlice()
+	sort.Slice(resources, func(i, j int) bool {
+		return construct.ResourceIdLess(resources[i].ID, resources[j].ID)
+	})
+	return placer.PlaceResources(resource, action.Step, resources, &action.numNeeded)
 }
 
 func (action *operationalResourceAction) doesResourceSatisfyNamespace(stepResource *construct.Resource, resource *construct.Resource) (bool, error) {
@@ -433,22 +387,34 @@ func (action *operationalResourceAction) createAndAddDependency(res, stepResourc
 }
 
 func (action *operationalResourceAction) generateResourceName(resourceToSet *construct.ResourceId, resource construct.ResourceId) error {
-	if resourceToSet.Name == "" {
-		numResources := 0
-		ids, err := construct.ToplogicalSort(action.ruleCtx.Solution.DataflowGraph())
-		if err != nil {
-			return err
-		}
-		for _, id := range ids {
-			if id.Type == resourceToSet.Type {
-				numResources++
-			}
+	if resourceToSet.Name != "" {
+		return nil
+	}
+	if action.Step.Unique {
+		// If creating unique resources, don't need to count the total resources because the owner's name is added
+		// which adds enough uniqueness against other resources in the graph. Just need to handle when the owner
+		// creates multiple resources of the same type.
+		suffix := ""
+		if action.Step.NumNeeded > 1 {
+			// If we are creating multiple resources, we want to append the number of resources we have created so far
+			// so that the names are unique.
+			suffix = fmt.Sprintf("-%d", action.Step.NumNeeded-action.numNeeded)
 		}
-		if action.Step.Unique {
-			resourceToSet.Name = fmt.Sprintf("%s-%s-%d", resourceToSet.Type, resource.Name, numResources)
-		} else {
-			resourceToSet.Name = fmt.Sprintf("%s-%d", resourceToSet.Type, numResources)
+		resourceToSet.Name = fmt.Sprintf("%s-%s%s", resource.Name, resourceToSet.Type, suffix)
+		return nil
+	}
+	numResources := 0
+	ids, err := construct.ToplogicalSort(action.ruleCtx.Solution.DataflowGraph())
+	if err != nil {
+		return err
+	}
+	matcher := construct.ResourceId{Provider: resourceToSet.Provider, Type: resourceToSet.Type, Namespace: resourceToSet.Namespace}
+	for _, id := range ids {
+		if matcher.Matches(id) {
+			numResources++
 		}
 	}
+	resourceToSet.Name = fmt.Sprintf("%s-%d", resourceToSet.Type, numResources)
+
 	return nil
 }
diff --git a/pkg/engine2/path_selection/path_expansion.go b/pkg/engine2/path_selection/path_expansion.go
index 257ae936..2702e1d4 100644
--- a/pkg/engine2/path_selection/path_expansion.go
+++ b/pkg/engine2/path_selection/path_expansion.go
@@ -362,14 +362,12 @@ func ExpandPath(
 		errs = errors.Join(errs, fmt.Errorf("error during raw view walk graph: %w", err))
 	}
 
-	predecessors, err := ctx.DataflowGraph().PredecessorMap()
+	edges, err := ctx.DataflowGraph().Edges()
 	if err != nil {
 		errs = errors.Join(errs, err)
 	}
-
-	adjacent, err := ctx.DataflowGraph().AdjacencyMap()
-	if err != nil {
-		errs = errors.Join(errs, err)
+	if errs != nil {
+		return errs
 	}
 
 	// addEdge checks whether the edge should be added according to the following rules:
@@ -389,21 +387,8 @@ func ExpandPath(
 			errs = errors.Join(errs, fmt.Errorf("could not find edge template for %s -> %s", source.id, target.id))
 			return
 		}
-		if tmpl.Unique.Target {
-			pred := predecessors[target.id]
-			for origSource := range pred {
-				if tmpl.Source.Matches(origSource) && origSource != source.id {
-					return
-				}
-			}
-		}
-		if tmpl.Unique.Source {
-			adj := adjacent[source.id]
-			for origTarget := range adj {
-				if tmpl.Target.Matches(origTarget) && origTarget != target.id {
-					return
-				}
-			}
+		if !tmpl.Unique.CanAdd(edges, source.id, target.id) {
+			return
 		}
 
 		valid, err := checkUniquenessValidity(ctx, source.id, target.id)
diff --git a/pkg/knowledge_base2/edge_template.go b/pkg/knowledge_base2/edge_template.go
index 062ebe90..33fff559 100644
--- a/pkg/knowledge_base2/edge_template.go
+++ b/pkg/knowledge_base2/edge_template.go
@@ -151,3 +151,40 @@ func (u *Unique) UnmarshalYAML(n *yaml.Node) error {
 
 	return fmt.Errorf("could not decode 'unique' field")
 }
+
+// CanAdd returns whether the edge source -> target can be added based on the uniqueness rules.
+// - "many-to-many" always returns true
+// - "one-to-many" returns true if the target does not have any edges that match the source type
+// - "many-to-one" returns true if the source does not have any edges that match the target type
+// - "one-to-one" returns true if neither the source nor the target have any edges that match the other type
+func (u Unique) CanAdd(edges []construct.Edge, source, target construct.ResourceId) bool {
+	if !u.Source && !u.Target {
+		return true
+	}
+
+	if u.Source { // one-to-many or one-to-one
+		sourceSel := construct.ResourceId{Provider: source.Provider, Type: source.Type}
+		for _, e := range edges {
+			if e.Target != target {
+				continue
+			}
+			// Make sure that the target doesn't have any edges that match the source type
+			if sourceSel.Matches(e.Source) && e.Source != source {
+				return false
+			}
+		}
+	}
+	if u.Target { // many-to-one or one-to-one
+		targetSel := construct.ResourceId{Provider: target.Provider, Type: target.Type}
+		for _, e := range edges {
+			if e.Source != source {
+				continue
+			}
+			// Make sure that the source doesn't have any edges that match the target type
+			if targetSel.Matches(e.Target) && e.Target != target {
+				return false
+			}
+		}
+	}
+	return true
+}
diff --git a/pkg/templates/aws/edges/nat_gateway-subnet.yaml b/pkg/templates/aws/edges/nat_gateway-subnet.yaml
index 1de6ea86..9be6cb58 100644
--- a/pkg/templates/aws/edges/nat_gateway-subnet.yaml
+++ b/pkg/templates/aws/edges/nat_gateway-subnet.yaml
@@ -1,2 +1,3 @@
 source: aws:nat_gateway
 target: aws:subnet
+unique: many-to-one
diff --git a/pkg/templates/aws/edges/route_table_association-route_table.yaml b/pkg/templates/aws/edges/route_table_association-route_table.yaml
index d87cf1f5..ad9da70c 100644
--- a/pkg/templates/aws/edges/route_table_association-route_table.yaml
+++ b/pkg/templates/aws/edges/route_table_association-route_table.yaml
@@ -1,9 +1,8 @@
 source: aws:route_table_association
 target: aws:route_table
 
-unique:
-  source: true
-  
+unique: many-to-one
+
 operational_rules:
   - if: '{{ eq (fieldValue "Type" (upstream "aws:subnet" .Source)) "private" }}'
     steps:
diff --git a/pkg/templates/aws/edges/subnet-availability_zone.yaml b/pkg/templates/aws/edges/subnet-availability_zone.yaml
index 08971006..23a344c9 100644
--- a/pkg/templates/aws/edges/subnet-availability_zone.yaml
+++ b/pkg/templates/aws/edges/subnet-availability_zone.yaml
@@ -2,4 +2,4 @@ source: aws:subnet
 target: aws:availability_zone
 
 unique:
-  target: true
\ No newline at end of file
+  target: true


Test patch:
diff --git a/.github/workflows/run-integ-tests.yaml b/.github/workflows/run-integ-tests.yaml
index 6199540a..5bbbf097 100644
--- a/.github/workflows/run-integ-tests.yaml
+++ b/.github/workflows/run-integ-tests.yaml
@@ -97,7 +97,7 @@ jobs:
         uses: actions/checkout@v3
       - uses: actions/setup-go@v3
         with:
-          go-version: '1.21.4'
+          go-version: '1.21.5'
       - name: pre-build
         if: ${{ inputs.pre-build-script }}
         run: ${{ inputs.pre-build-script }}
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index ea66809e..40fc08c3 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -15,7 +15,7 @@ jobs:
     steps:
       - uses: actions/setup-go@v3
         with:
-          go-version: '1.21.4'
+          go-version: '1.21.5'
       - uses: actions/checkout@v3
       - uses: actions/cache@v2
         with:
diff --git a/pkg/engine2/operational_rule/operational_action_test.go b/pkg/engine2/operational_rule/operational_action_test.go
index 6409eac2..4d0921bc 100644
--- a/pkg/engine2/operational_rule/operational_action_test.go
+++ b/pkg/engine2/operational_rule/operational_action_test.go
@@ -59,10 +59,10 @@ func Test_handleOperationalResourceAction(t *testing.T) {
 				},
 				numNeeded: 1,
 			},
-			resource:     &construct.Resource{ID: graphtest.ParseId(t, "test:step:test")},
-			initialState: []any{"test:step:test"},
+			resource:     &construct.Resource{ID: graphtest.ParseId(t, "test:step:resource")},
+			initialState: []any{"test:step:resource"},
 			want: enginetesting.ExpectedGraphs{
-				Dataflow: []any{"test:step:test", "test:test:test-test-0", "test:step:test -> test:test:test-test-0"},
+				Dataflow: []any{"test:step:resource -> test:test:resource-test"},
 			},
 		},
 		{
diff --git a/pkg/engine2/testdata/2_routes.expect.yaml b/pkg/engine2/testdata/2_routes.expect.yaml
index 5489d9bc..89a2ef1c 100755
--- a/pkg/engine2/testdata/2_routes.expect.yaml
+++ b/pkg/engine2/testdata/2_routes.expect.yaml
@@ -49,8 +49,8 @@ resources:
     aws:api_integration:rest_api_1:integ0:
         IntegrationHttpMethod: POST
         Method: aws:api_method:rest_api_1:api_method-0
-        Resource: aws:api_resource:rest_api_1:api_resource-0
         RequestParameters: {}
+        Resource: aws:api_resource:rest_api_1:api_resource-0
         RestApi: aws:rest_api:rest_api_1
         Route: /lambda0/api
         Target: aws:lambda_function:lambda_function_0
diff --git a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
index f2bc779f..801d089c 100755
--- a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
@@ -1,7 +1,7 @@
 provider: aws
 resources:
   rds_instance/rds-instance-2:
-    children: aws:rds_subnet_group:rds_subnet_group-0,aws:security_group:vpc-0:security_group-rds-instance-2-1
+    children: aws:rds_subnet_group:rds_subnet_group-0,aws:security_group:vpc-0:rds-instance-2-security_group
     parent: vpc/vpc-0
 
 
diff --git a/pkg/engine2/testdata/ecs_rds.expect.yaml b/pkg/engine2/testdata/ecs_rds.expect.yaml
index fd92146d..aa5311cc 100755
--- a/pkg/engine2/testdata/ecs_rds.expect.yaml
+++ b/pkg/engine2/testdata/ecs_rds.expect.yaml
@@ -1,5 +1,5 @@
 resources:
-    aws:security_group:vpc-0:security_group-ecs_service_0-0:
+    aws:security_group:vpc-0:ecs_service_0-security_group:
         EgressRules:
             - CidrBlocks:
                 - 0.0.0.0/0
@@ -21,7 +21,7 @@ resources:
         ForceNewDeployment: true
         LaunchType: FARGATE
         SecurityGroups:
-            - aws:security_group:vpc-0:security_group-ecs_service_0-0
+            - aws:security_group:vpc-0:ecs_service_0-security_group
         Subnets:
             - aws:subnet:vpc-0:subnet-0
             - aws:subnet:vpc-0:subnet-1
@@ -78,22 +78,22 @@ resources:
         RetentionInDays: 5
     aws:ecr_repo:ecr_repo-0:
         ForceDelete: true
-    aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0:
-    aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1:
-    aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0:
-        ElasticIp: aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0
+    aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip:
+    aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip:
+    aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway:
+        ElasticIp: aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
         Subnet: aws:subnet:vpc-0:subnet-2
     aws:subnet:vpc-0:subnet-2:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
         CidrBlock: 10.0.0.0/18
         MapPublicIpOnLaunch: false
-        RouteTable: aws:route_table:route_table-subnet-2-2
+        RouteTable: aws:route_table:subnet-2-route_table
         Type: public
         Vpc: aws:vpc:vpc-0
-    aws:route_table_association:subnet-2-route_table-subnet-2-2:
-        RouteTable: aws:route_table:route_table-subnet-2-2
+    aws:route_table_association:subnet-2-subnet-2-route_table:
+        RouteTable: aws:route_table:subnet-2-route_table
         Subnet: aws:subnet:vpc-0:subnet-2
-    aws:route_table:route_table-subnet-2-2:
+    aws:route_table:subnet-2-route_table:
         Routes:
             - CidrBlock: 0.0.0.0/0
               Gateway: aws:internet_gateway:vpc-0:internet_gateway-0
@@ -103,20 +103,20 @@ resources:
         Region: aws:region:region-0
     aws:internet_gateway:vpc-0:internet_gateway-0:
         Vpc: aws:vpc:vpc-0
-    aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1:
-        ElasticIp: aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway:
+        ElasticIp: aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
         Subnet: aws:subnet:vpc-0:subnet-3
     aws:subnet:vpc-0:subnet-3:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-1
         CidrBlock: 10.0.64.0/18
         MapPublicIpOnLaunch: false
-        RouteTable: aws:route_table:route_table-subnet-3-3
+        RouteTable: aws:route_table:subnet-3-route_table
         Type: public
         Vpc: aws:vpc:vpc-0
-    aws:route_table_association:subnet-3-route_table-subnet-3-3:
-        RouteTable: aws:route_table:route_table-subnet-3-3
+    aws:route_table_association:subnet-3-subnet-3-route_table:
+        RouteTable: aws:route_table:subnet-3-route_table
         Subnet: aws:subnet:vpc-0:subnet-3
-    aws:route_table:route_table-subnet-3-3:
+    aws:route_table:subnet-3-route_table:
         Routes:
             - CidrBlock: 0.0.0.0/0
               Gateway: aws:internet_gateway:vpc-0:internet_gateway-0
@@ -133,7 +133,7 @@ resources:
         IamDatabaseAuthenticationEnabled: true
         InstanceClass: db.t3.micro
         SecurityGroups:
-            - aws:security_group:vpc-0:security_group-rds-instance-2-1
+            - aws:security_group:vpc-0:rds-instance-2-security_group
         SkipFinalSnapshot: true
         SubnetGroup: aws:rds_subnet_group:rds_subnet_group-0
     aws:rds_subnet_group:rds_subnet_group-0:
@@ -144,23 +144,23 @@ resources:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
         CidrBlock: 10.0.128.0/18
         MapPublicIpOnLaunch: false
-        RouteTable: aws:route_table:route_table-subnet-0-0
+        RouteTable: aws:route_table:subnet-0-route_table
         Type: private
         Vpc: aws:vpc:vpc-0
     aws:subnet:vpc-0:subnet-1:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-1
         CidrBlock: 10.0.192.0/18
         MapPublicIpOnLaunch: false
-        RouteTable: aws:route_table:route_table-subnet-1-1
+        RouteTable: aws:route_table:subnet-1-route_table
         Type: private
         Vpc: aws:vpc:vpc-0
-    aws:route_table_association:subnet-0-route_table-subnet-0-0:
-        RouteTable: aws:route_table:route_table-subnet-0-0
+    aws:route_table_association:subnet-0-subnet-0-route_table:
+        RouteTable: aws:route_table:subnet-0-route_table
         Subnet: aws:subnet:vpc-0:subnet-0
-    aws:route_table_association:subnet-1-route_table-subnet-1-1:
-        RouteTable: aws:route_table:route_table-subnet-1-1
+    aws:route_table_association:subnet-1-subnet-1-route_table:
+        RouteTable: aws:route_table:subnet-1-route_table
         Subnet: aws:subnet:vpc-0:subnet-1
-    aws:security_group:vpc-0:security_group-rds-instance-2-1:
+    aws:security_group:vpc-0:rds-instance-2-security_group:
         EgressRules:
             - CidrBlocks:
                 - 0.0.0.0/0
@@ -169,6 +169,11 @@ resources:
               Protocol: "-1"
               ToPort: 0
         IngressRules:
+            - Description: Allow ingress traffic from within the same security group
+              FromPort: 0
+              Protocol: "-1"
+              Self: true
+              ToPort: 0
             - CidrBlocks:
                 - 10.0.128.0/18
               Description: Allow ingress traffic from ip addresses within the subnet subnet-0
@@ -181,29 +186,24 @@ resources:
               FromPort: 0
               Protocol: "-1"
               ToPort: 0
-            - Description: Allow ingress traffic from within the same security group
-              FromPort: 0
-              Protocol: "-1"
-              Self: true
-              ToPort: 0
         Vpc: aws:vpc:vpc-0
-    aws:route_table:route_table-subnet-0-0:
+    aws:route_table:subnet-0-route_table:
         Routes:
             - CidrBlock: 0.0.0.0/0
-              NatGateway: aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0
+              NatGateway: aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
         Vpc: aws:vpc:vpc-0
-    aws:route_table:route_table-subnet-1-1:
+    aws:route_table:subnet-1-route_table:
         Routes:
             - CidrBlock: 0.0.0.0/0
-              NatGateway: aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1
+              NatGateway: aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
         Vpc: aws:vpc:vpc-0
     aws:vpc:vpc-0:
         CidrBlock: 10.0.0.0/16
         EnableDnsHostnames: true
         EnableDnsSupport: true
 edges:
-    aws:security_group:vpc-0:security_group-ecs_service_0-0 -> aws:ecs_service:ecs_service_0:
-    aws:security_group:vpc-0:security_group-ecs_service_0-0 -> aws:vpc:vpc-0:
+    aws:security_group:vpc-0:ecs_service_0-security_group -> aws:ecs_service:ecs_service_0:
+    aws:security_group:vpc-0:ecs_service_0-security_group -> aws:vpc:vpc-0:
     aws:ecs_service:ecs_service_0 -> aws:ecs_cluster:ecs_cluster-0:
     aws:ecs_service:ecs_service_0 -> aws:ecs_task_definition:ecs_service_0:
     aws:ecs_service:ecs_service_0 -> aws:subnet:vpc-0:subnet-0:
@@ -214,43 +214,41 @@ edges:
     aws:ecs_task_definition:ecs_service_0 -> aws:region:region-0:
     aws:ecr_image:ecs_service_0-image -> aws:ecr_repo:ecr_repo-0:
     aws:iam_role:ecs_service_0-rds-instance-2 -> aws:rds_instance:rds-instance-2:
-    ? aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0 -> aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0
-    :
-    aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0 -> aws:subnet:vpc-0:subnet-2:
+    aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway -> aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip:
+    aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway -> aws:subnet:vpc-0:subnet-2:
     aws:subnet:vpc-0:subnet-2 -> aws:availability_zone:region-0:availability_zone-0:
-    aws:subnet:vpc-0:subnet-2 -> aws:route_table_association:subnet-2-route_table-subnet-2-2:
+    aws:subnet:vpc-0:subnet-2 -> aws:route_table_association:subnet-2-subnet-2-route_table:
     aws:subnet:vpc-0:subnet-2 -> aws:vpc:vpc-0:
-    aws:route_table_association:subnet-2-route_table-subnet-2-2 -> aws:route_table:route_table-subnet-2-2:
-    aws:route_table:route_table-subnet-2-2 -> aws:internet_gateway:vpc-0:internet_gateway-0:
-    aws:route_table:route_table-subnet-2-2 -> aws:vpc:vpc-0:
+    aws:route_table_association:subnet-2-subnet-2-route_table -> aws:route_table:subnet-2-route_table:
+    aws:route_table:subnet-2-route_table -> aws:internet_gateway:vpc-0:internet_gateway-0:
+    aws:route_table:subnet-2-route_table -> aws:vpc:vpc-0:
     aws:availability_zone:region-0:availability_zone-0 -> aws:region:region-0:
     aws:internet_gateway:vpc-0:internet_gateway-0 -> aws:vpc:vpc-0:
-    ? aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1 -> aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1
-    :
-    aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1 -> aws:subnet:vpc-0:subnet-3:
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway -> aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip:
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway -> aws:subnet:vpc-0:subnet-3:
     aws:subnet:vpc-0:subnet-3 -> aws:availability_zone:region-0:availability_zone-1:
-    aws:subnet:vpc-0:subnet-3 -> aws:route_table_association:subnet-3-route_table-subnet-3-3:
+    aws:subnet:vpc-0:subnet-3 -> aws:route_table_association:subnet-3-subnet-3-route_table:
     aws:subnet:vpc-0:subnet-3 -> aws:vpc:vpc-0:
-    aws:route_table_association:subnet-3-route_table-subnet-3-3 -> aws:route_table:route_table-subnet-3-3:
-    aws:route_table:route_table-subnet-3-3 -> aws:internet_gateway:vpc-0:internet_gateway-0:
-    aws:route_table:route_table-subnet-3-3 -> aws:vpc:vpc-0:
+    aws:route_table_association:subnet-3-subnet-3-route_table -> aws:route_table:subnet-3-route_table:
+    aws:route_table:subnet-3-route_table -> aws:internet_gateway:vpc-0:internet_gateway-0:
+    aws:route_table:subnet-3-route_table -> aws:vpc:vpc-0:
     aws:availability_zone:region-0:availability_zone-1 -> aws:region:region-0:
     aws:rds_instance:rds-instance-2 -> aws:rds_subnet_group:rds_subnet_group-0:
     aws:rds_subnet_group:rds_subnet_group-0 -> aws:subnet:vpc-0:subnet-0:
     aws:rds_subnet_group:rds_subnet_group-0 -> aws:subnet:vpc-0:subnet-1:
     aws:subnet:vpc-0:subnet-0 -> aws:availability_zone:region-0:availability_zone-0:
-    aws:subnet:vpc-0:subnet-0 -> aws:route_table_association:subnet-0-route_table-subnet-0-0:
-    aws:subnet:vpc-0:subnet-0 -> aws:security_group:vpc-0:security_group-rds-instance-2-1:
+    aws:subnet:vpc-0:subnet-0 -> aws:route_table_association:subnet-0-subnet-0-route_table:
+    aws:subnet:vpc-0:subnet-0 -> aws:security_group:vpc-0:rds-instance-2-security_group:
     aws:subnet:vpc-0:subnet-0 -> aws:vpc:vpc-0:
     aws:subnet:vpc-0:subnet-1 -> aws:availability_zone:region-0:availability_zone-1:
-    aws:subnet:vpc-0:subnet-1 -> aws:route_table_association:subnet-1-route_table-subnet-1-1:
-    aws:subnet:vpc-0:subnet-1 -> aws:security_group:vpc-0:security_group-rds-instance-2-1:
+    aws:subnet:vpc-0:subnet-1 -> aws:route_table_association:subnet-1-subnet-1-route_table:
+    aws:subnet:vpc-0:subnet-1 -> aws:security_group:vpc-0:rds-instance-2-security_group:
     aws:subnet:vpc-0:subnet-1 -> aws:vpc:vpc-0:
-    aws:route_table_association:subnet-0-route_table-subnet-0-0 -> aws:route_table:route_table-subnet-0-0:
-    aws:route_table_association:subnet-1-route_table-subnet-1-1 -> aws:route_table:route_table-subnet-1-1:
-    aws:security_group:vpc-0:security_group-rds-instance-2-1 -> aws:rds_instance:rds-instance-2:
-    aws:security_group:vpc-0:security_group-rds-instance-2-1 -> aws:vpc:vpc-0:
-    aws:route_table:route_table-subnet-0-0 -> aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0:
-    aws:route_table:route_table-subnet-0-0 -> aws:vpc:vpc-0:
-    aws:route_table:route_table-subnet-1-1 -> aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1:
-    aws:route_table:route_table-subnet-1-1 -> aws:vpc:vpc-0:
+    aws:route_table_association:subnet-0-subnet-0-route_table -> aws:route_table:subnet-0-route_table:
+    aws:route_table_association:subnet-1-subnet-1-route_table -> aws:route_table:subnet-1-route_table:
+    aws:security_group:vpc-0:rds-instance-2-security_group -> aws:rds_instance:rds-instance-2:
+    aws:security_group:vpc-0:rds-instance-2-security_group -> aws:vpc:vpc-0:
+    aws:route_table:subnet-0-route_table -> aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway:
+    aws:route_table:subnet-0-route_table -> aws:vpc:vpc-0:
+    aws:route_table:subnet-1-route_table -> aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway:
+    aws:route_table:subnet-1-route_table -> aws:vpc:vpc-0:
diff --git a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
index 8faa223f..7e272c98 100755
--- a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
@@ -7,33 +7,33 @@ resources:
   aws:availability_zone:region-0/availability_zone-0:
   aws:availability_zone:region-0/availability_zone-0 -> region/region-0:
 
-  aws:security_group:vpc-0/security_group-rds-instance-2-1:
-  aws:security_group:vpc-0/security_group-rds-instance-2-1 -> vpc/vpc-0:
+  aws:security_group:vpc-0/rds-instance-2-security_group:
+  aws:security_group:vpc-0/rds-instance-2-security_group -> vpc/vpc-0:
 
   aws:availability_zone:region-0/availability_zone-1:
   aws:availability_zone:region-0/availability_zone-1 -> region/region-0:
 
   aws:subnet:vpc-0/subnet-0:
   aws:subnet:vpc-0/subnet-0 -> aws:availability_zone:region-0/availability_zone-0:
-  aws:subnet:vpc-0/subnet-0 -> aws:security_group:vpc-0/security_group-rds-instance-2-1:
+  aws:subnet:vpc-0/subnet-0 -> aws:security_group:vpc-0/rds-instance-2-security_group:
   aws:subnet:vpc-0/subnet-0 -> vpc/vpc-0:
 
   aws:subnet:vpc-0/subnet-1:
   aws:subnet:vpc-0/subnet-1 -> aws:availability_zone:region-0/availability_zone-1:
-  aws:subnet:vpc-0/subnet-1 -> aws:security_group:vpc-0/security_group-rds-instance-2-1:
+  aws:subnet:vpc-0/subnet-1 -> aws:security_group:vpc-0/rds-instance-2-security_group:
   aws:subnet:vpc-0/subnet-1 -> vpc/vpc-0:
 
   rds_subnet_group/rds_subnet_group-0:
   rds_subnet_group/rds_subnet_group-0 -> aws:subnet:vpc-0/subnet-0:
   rds_subnet_group/rds_subnet_group-0 -> aws:subnet:vpc-0/subnet-1:
 
-  elastic_ip/elastic_ip-nat_gateway-route_table-subnet-1-1-1-1:
+  elastic_ip/subnet-1-route_table-nat_gateway-elastic_ip:
 
   aws:subnet:vpc-0/subnet-3:
   aws:subnet:vpc-0/subnet-3 -> aws:availability_zone:region-0/availability_zone-1:
   aws:subnet:vpc-0/subnet-3 -> vpc/vpc-0:
 
-  elastic_ip/elastic_ip-nat_gateway-route_table-subnet-0-0-0-0:
+  elastic_ip/subnet-0-route_table-nat_gateway-elastic_ip:
 
   aws:subnet:vpc-0/subnet-2:
   aws:subnet:vpc-0/subnet-2 -> aws:availability_zone:region-0/availability_zone-0:
@@ -43,18 +43,18 @@ resources:
 
   rds_instance/rds-instance-2:
   rds_instance/rds-instance-2 -> rds_subnet_group/rds_subnet_group-0:
-  rds_instance/rds-instance-2 -> aws:security_group:vpc-0/security_group-rds-instance-2-1:
+  rds_instance/rds-instance-2 -> aws:security_group:vpc-0/rds-instance-2-security_group:
 
   aws:internet_gateway:vpc-0/internet_gateway-0:
   aws:internet_gateway:vpc-0/internet_gateway-0 -> vpc/vpc-0:
 
-  aws:nat_gateway:subnet-3/nat_gateway-route_table-subnet-1-1-1:
-  aws:nat_gateway:subnet-3/nat_gateway-route_table-subnet-1-1-1 -> elastic_ip/elastic_ip-nat_gateway-route_table-subnet-1-1-1-1:
-  aws:nat_gateway:subnet-3/nat_gateway-route_table-subnet-1-1-1 -> aws:subnet:vpc-0/subnet-3:
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway:
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway -> elastic_ip/subnet-1-route_table-nat_gateway-elastic_ip:
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway -> aws:subnet:vpc-0/subnet-3:
 
-  aws:nat_gateway:subnet-2/nat_gateway-route_table-subnet-0-0-0:
-  aws:nat_gateway:subnet-2/nat_gateway-route_table-subnet-0-0-0 -> elastic_ip/elastic_ip-nat_gateway-route_table-subnet-0-0-0-0:
-  aws:nat_gateway:subnet-2/nat_gateway-route_table-subnet-0-0-0 -> aws:subnet:vpc-0/subnet-2:
+  aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway:
+  aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway -> elastic_ip/subnet-0-route_table-nat_gateway-elastic_ip:
+  aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway -> aws:subnet:vpc-0/subnet-2:
 
   ecr_image/ecs_service_0-image:
   ecr_image/ecs_service_0-image -> ecr_repo/ecr_repo-0:
@@ -64,21 +64,21 @@ resources:
 
   log_group/ecs_service_0-log-group:
 
-  route_table/route_table-subnet-3-3:
-  route_table/route_table-subnet-3-3 -> aws:internet_gateway:vpc-0/internet_gateway-0:
-  route_table/route_table-subnet-3-3 -> vpc/vpc-0:
+  route_table/subnet-3-route_table:
+  route_table/subnet-3-route_table -> aws:internet_gateway:vpc-0/internet_gateway-0:
+  route_table/subnet-3-route_table -> vpc/vpc-0:
 
-  route_table/route_table-subnet-2-2:
-  route_table/route_table-subnet-2-2 -> aws:internet_gateway:vpc-0/internet_gateway-0:
-  route_table/route_table-subnet-2-2 -> vpc/vpc-0:
+  route_table/subnet-2-route_table:
+  route_table/subnet-2-route_table -> aws:internet_gateway:vpc-0/internet_gateway-0:
+  route_table/subnet-2-route_table -> vpc/vpc-0:
 
-  route_table/route_table-subnet-1-1:
-  route_table/route_table-subnet-1-1 -> aws:nat_gateway:subnet-3/nat_gateway-route_table-subnet-1-1-1:
-  route_table/route_table-subnet-1-1 -> vpc/vpc-0:
+  route_table/subnet-1-route_table:
+  route_table/subnet-1-route_table -> aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway:
+  route_table/subnet-1-route_table -> vpc/vpc-0:
 
-  route_table/route_table-subnet-0-0:
-  route_table/route_table-subnet-0-0 -> aws:nat_gateway:subnet-2/nat_gateway-route_table-subnet-0-0-0:
-  route_table/route_table-subnet-0-0 -> vpc/vpc-0:
+  route_table/subnet-0-route_table:
+  route_table/subnet-0-route_table -> aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway:
+  route_table/subnet-0-route_table -> vpc/vpc-0:
 
   ecs_cluster/ecs_cluster-0:
 
@@ -88,29 +88,29 @@ resources:
   ecs_task_definition/ecs_service_0 -> log_group/ecs_service_0-log-group:
   ecs_task_definition/ecs_service_0 -> region/region-0:
 
-  aws:security_group:vpc-0/security_group-ecs_service_0-0:
-  aws:security_group:vpc-0/security_group-ecs_service_0-0 -> vpc/vpc-0:
+  aws:security_group:vpc-0/ecs_service_0-security_group:
+  aws:security_group:vpc-0/ecs_service_0-security_group -> vpc/vpc-0:
 
-  route_table_association/subnet-3-route_table-subnet-3-3:
-  route_table_association/subnet-3-route_table-subnet-3-3 -> route_table/route_table-subnet-3-3:
-  route_table_association/subnet-3-route_table-subnet-3-3 -> aws:subnet:vpc-0/subnet-3:
+  route_table_association/subnet-3-subnet-3-route_table:
+  route_table_association/subnet-3-subnet-3-route_table -> route_table/subnet-3-route_table:
+  route_table_association/subnet-3-subnet-3-route_table -> aws:subnet:vpc-0/subnet-3:
 
-  route_table_association/subnet-2-route_table-subnet-2-2:
-  route_table_association/subnet-2-route_table-subnet-2-2 -> route_table/route_table-subnet-2-2:
-  route_table_association/subnet-2-route_table-subnet-2-2 -> aws:subnet:vpc-0/subnet-2:
+  route_table_association/subnet-2-subnet-2-route_table:
+  route_table_association/subnet-2-subnet-2-route_table -> route_table/subnet-2-route_table:
+  route_table_association/subnet-2-subnet-2-route_table -> aws:subnet:vpc-0/subnet-2:
 
-  route_table_association/subnet-1-route_table-subnet-1-1:
-  route_table_association/subnet-1-route_table-subnet-1-1 -> route_table/route_table-subnet-1-1:
-  route_table_association/subnet-1-route_table-subnet-1-1 -> aws:subnet:vpc-0/subnet-1:
+  route_table_association/subnet-1-subnet-1-route_table:
+  route_table_association/subnet-1-subnet-1-route_table -> route_table/subnet-1-route_table:
+  route_table_association/subnet-1-subnet-1-route_table -> aws:subnet:vpc-0/subnet-1:
 
-  route_table_association/subnet-0-route_table-subnet-0-0:
-  route_table_association/subnet-0-route_table-subnet-0-0 -> route_table/route_table-subnet-0-0:
-  route_table_association/subnet-0-route_table-subnet-0-0 -> aws:subnet:vpc-0/subnet-0:
+  route_table_association/subnet-0-subnet-0-route_table:
+  route_table_association/subnet-0-subnet-0-route_table -> route_table/subnet-0-route_table:
+  route_table_association/subnet-0-subnet-0-route_table -> aws:subnet:vpc-0/subnet-0:
 
   ecs_service/ecs_service_0:
   ecs_service/ecs_service_0 -> ecs_cluster/ecs_cluster-0:
   ecs_service/ecs_service_0 -> ecs_task_definition/ecs_service_0:
-  ecs_service/ecs_service_0 -> aws:security_group:vpc-0/security_group-ecs_service_0-0:
+  ecs_service/ecs_service_0 -> aws:security_group:vpc-0/ecs_service_0-security_group:
   ecs_service/ecs_service_0 -> aws:subnet:vpc-0/subnet-0:
   ecs_service/ecs_service_0 -> aws:subnet:vpc-0/subnet-1:
 
diff --git a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
index d6e6c393..5a612b6c 100755
--- a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
@@ -24,7 +24,7 @@ resources:
   rest_api/rest_api_4:
 
   eks_cluster/eks_cluster-0:
-    children: aws:iam_role:ClusterRole-eks_cluster-0,aws:security_group:vpc-0:security_group-eks_cluster-0-0
+    children: aws:iam_role:ClusterRole-eks_cluster-0,aws:security_group:vpc-0:eks_cluster-0-security_group
     parent: vpc/vpc-0
 
 
diff --git a/pkg/engine2/testdata/k8s_api.expect.yaml b/pkg/engine2/testdata/k8s_api.expect.yaml
index b3f245a1..2000c7f9 100755
--- a/pkg/engine2/testdata/k8s_api.expect.yaml
+++ b/pkg/engine2/testdata/k8s_api.expect.yaml
@@ -20,7 +20,7 @@ resources:
         Cluster: aws:eks_cluster:eks_cluster-0
         Internal: true
         Repo: https://kubernetes-sigs.github.io/metrics-server/
-    kubernetes:kube_config:kube_config-eks_cluster-0-0:
+    kubernetes:kube_config:eks_cluster-0-kube_config:
         apiVersion: v1
         clusters:
             - cluster:
@@ -220,7 +220,7 @@ resources:
             spec:
                 automountServiceAccountToken: true
                 containers:
-                    - image: aws:ecr_image:ecr_image-pod2-0#ImageName
+                    - image: aws:ecr_image:pod2-ecr_image#ImageName
                       name: pod2
                       ports:
                         - containerPort: 80
@@ -238,9 +238,9 @@ resources:
                     Federated:
                         - aws:iam_oidc_provider:eks_cluster-0#Arn
             Version: "2012-10-17"
-    aws:ecr_image:ecr_image-pod2-0:
+    aws:ecr_image:pod2-ecr_image:
         Context: .
-        Dockerfile: ecr_image-pod2-0.Dockerfile
+        Dockerfile: pod2-ecr_image.Dockerfile
         Repo: aws:ecr_repo:ecr_repo-0
     aws:eks_node_group:eks_node_group-0:
         AmiType: AL2_x86_64
@@ -252,7 +252,7 @@ resources:
         MaxSize: 3
         MaxUnavailable: 1
         MinSize: 1
-        NodeRole: aws:iam_role:iam_role-eks_node_group-0-2
+        NodeRole: aws:iam_role:eks_node_group-0-iam_role
         Subnets:
             - aws:subnet:vpc-0:subnet-0
             - aws:subnet:vpc-0:subnet-1
@@ -274,7 +274,7 @@ resources:
         Role: aws:iam_role:aws-load-balancer-controller
     aws:ecr_repo:ecr_repo-0:
         ForceDelete: true
-    aws:iam_role:iam_role-eks_node_group-0-2:
+    aws:iam_role:eks_node_group-0-iam_role:
         AssumeRolePolicyDoc:
             Statement:
                 - Action:
@@ -285,12 +285,12 @@ resources:
                         - ec2.amazonaws.com
             Version: "2012-10-17"
         ManagedPolicies:
-            - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
-            - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
             - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
             - arn:aws:iam::aws:policy/AWSCloudMapFullAccess
             - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
             - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
+            - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
+            - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
     aws:iam_role:pod2:
         AssumeRolePolicyDoc:
             Statement:
@@ -454,9 +454,9 @@ resources:
         Region: aws:region:region-0
     aws:eks_cluster:eks_cluster-0:
         ClusterRole: aws:iam_role:ClusterRole-eks_cluster-0
-        KubeConfig: kubernetes:kube_config:kube_config-eks_cluster-0-0
+        KubeConfig: kubernetes:kube_config:eks_cluster-0-kube_config
         SecurityGroups:
-            - aws:security_group:vpc-0:security_group-eks_cluster-0-0
+            - aws:security_group:vpc-0:eks_cluster-0-security_group
         Subnets:
             - aws:subnet:vpc-0:subnet-0
             - aws:subnet:vpc-0:subnet-1
@@ -477,23 +477,23 @@ resources:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
         CidrBlock: 10.0.128.0/18
         MapPublicIpOnLaunch: false
-        RouteTable: aws:route_table:route_table-subnet-0-0
+        RouteTable: aws:route_table:subnet-0-route_table
         Type: private
         Vpc: aws:vpc:vpc-0
     aws:subnet:vpc-0:subnet-1:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-1
         CidrBlock: 10.0.192.0/18
         MapPublicIpOnLaunch: false
-        RouteTable: aws:route_table:route_table-subnet-1-1
+        RouteTable: aws:route_table:subnet-1-route_table
         Type: private
         Vpc: aws:vpc:vpc-0
-    aws:route_table_association:subnet-0-route_table-subnet-0-0:
-        RouteTable: aws:route_table:route_table-subnet-0-0
+    aws:route_table_association:subnet-0-subnet-0-route_table:
+        RouteTable: aws:route_table:subnet-0-route_table
         Subnet: aws:subnet:vpc-0:subnet-0
-    aws:route_table_association:subnet-1-route_table-subnet-1-1:
-        RouteTable: aws:route_table:route_table-subnet-1-1
+    aws:route_table_association:subnet-1-subnet-1-route_table:
+        RouteTable: aws:route_table:subnet-1-route_table
         Subnet: aws:subnet:vpc-0:subnet-1
-    aws:security_group:vpc-0:security_group-eks_cluster-0-0:
+    aws:security_group:vpc-0:eks_cluster-0-security_group:
         EgressRules:
             - CidrBlocks:
                 - 0.0.0.0/0
@@ -526,57 +526,57 @@ resources:
               Self: true
               ToPort: 0
         Vpc: aws:vpc:vpc-0
-    aws:route_table:route_table-subnet-0-0:
+    aws:route_table:subnet-0-route_table:
         Routes:
             - CidrBlock: 0.0.0.0/0
-              NatGateway: aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0
+              NatGateway: aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
         Vpc: aws:vpc:vpc-0
-    aws:route_table:route_table-subnet-1-1:
+    aws:route_table:subnet-1-route_table:
         Routes:
             - CidrBlock: 0.0.0.0/0
-              NatGateway: aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1
+              NatGateway: aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
         Vpc: aws:vpc:vpc-0
-    aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0:
-        ElasticIp: aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0
+    aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway:
+        ElasticIp: aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
         Subnet: aws:subnet:vpc-0:subnet-2
-    aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1:
-        ElasticIp: aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway:
+        ElasticIp: aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
         Subnet: aws:subnet:vpc-0:subnet-3
-    aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0:
+    aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip:
     aws:subnet:vpc-0:subnet-2:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
         CidrBlock: 10.0.0.0/18
         MapPublicIpOnLaunch: false
-        RouteTable: aws:route_table:route_table-subnet-2-2
+        RouteTable: aws:route_table:subnet-2-route_table
         Type: public
         Vpc: aws:vpc:vpc-0
-    aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1:
+    aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip:
     aws:subnet:vpc-0:subnet-3:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-1
         CidrBlock: 10.0.64.0/18
         MapPublicIpOnLaunch: false
-        RouteTable: aws:route_table:route_table-subnet-3-3
+        RouteTable: aws:route_table:subnet-3-route_table
         Type: public
         Vpc: aws:vpc:vpc-0
     aws:availability_zone:region-0:availability_zone-0:
         Index: 0
         Region: aws:region:region-0
-    aws:route_table_association:subnet-2-route_table-subnet-2-2:
-        RouteTable: aws:route_table:route_table-subnet-2-2
+    aws:route_table_association:subnet-2-subnet-2-route_table:
+        RouteTable: aws:route_table:subnet-2-route_table
         Subnet: aws:subnet:vpc-0:subnet-2
     aws:availability_zone:region-0:availability_zone-1:
         Index: 1
         Region: aws:region:region-0
-    aws:route_table_association:subnet-3-route_table-subnet-3-3:
-        RouteTable: aws:route_table:route_table-subnet-3-3
+    aws:route_table_association:subnet-3-subnet-3-route_table:
+        RouteTable: aws:route_table:subnet-3-route_table
         Subnet: aws:subnet:vpc-0:subnet-3
-    aws:route_table:route_table-subnet-2-2:
+    aws:route_table:subnet-2-route_table:
         Routes:
             - CidrBlock: 0.0.0.0/0
               Gateway: aws:internet_gateway:vpc-0:internet_gateway-0
         Vpc: aws:vpc:vpc-0
     aws:region:region-0:
-    aws:route_table:route_table-subnet-3-3:
+    aws:route_table:subnet-3-route_table:
         Routes:
             - CidrBlock: 0.0.0.0/0
               Gateway: aws:internet_gateway:vpc-0:internet_gateway-0
@@ -594,7 +594,7 @@ edges:
     aws:security_group_rule:security_group_rule-0 -> aws:vpc:vpc-0:
     kubernetes:helm_chart:eks_cluster-0:metricsserver -> aws:eks_cluster:eks_cluster-0:
     kubernetes:helm_chart:eks_cluster-0:metricsserver -> aws:eks_node_group:eks_node_group-0:
-    kubernetes:kube_config:kube_config-eks_cluster-0-0 -> aws:eks_cluster:eks_cluster-0:
+    kubernetes:kube_config:eks_cluster-0-kube_config -> aws:eks_cluster:eks_cluster-0:
     kubernetes:manifest:fluent-bit -> aws:eks_cluster:eks_cluster-0:
     kubernetes:manifest:fluent-bit -> kubernetes:config_map:fluent-bit-cluster-info:
     aws:api_deployment:rest_api_4:api_deployment-0 -> aws:api_integration:rest_api_4:rest_api_4_integration_0:
@@ -627,15 +627,15 @@ edges:
     kubernetes:service:restapi4integration0-pod2 -> kubernetes:pod:eks_cluster-0:pod2:
     kubernetes:service_account:eks_cluster-0:aws-load-balancer-controller -> aws:eks_cluster:eks_cluster-0:
     kubernetes:service_account:eks_cluster-0:aws-load-balancer-controller -> aws:iam_role:aws-load-balancer-controller:
-    kubernetes:pod:eks_cluster-0:pod2 -> aws:ecr_image:ecr_image-pod2-0:
+    kubernetes:pod:eks_cluster-0:pod2 -> aws:ecr_image:pod2-ecr_image:
     kubernetes:pod:eks_cluster-0:pod2 -> aws:eks_cluster:eks_cluster-0:
     kubernetes:pod:eks_cluster-0:pod2 -> aws:eks_node_group:eks_node_group-0:
     kubernetes:pod:eks_cluster-0:pod2 -> kubernetes:service_account:eks_cluster-0:pod2:
     aws:iam_role:aws-load-balancer-controller -> aws:iam_oidc_provider:eks_cluster-0:
     aws:iam_role:aws-load-balancer-controller -> aws:iam_role_policy_attachment:aws-load-balancer-controller-iam_policy-0:
-    aws:ecr_image:ecr_image-pod2-0 -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:pod2-ecr_image -> aws:ecr_repo:ecr_repo-0:
     aws:eks_node_group:eks_node_group-0 -> aws:eks_cluster:eks_cluster-0:
-    aws:eks_node_group:eks_node_group-0 -> aws:iam_role:iam_role-eks_node_group-0-2:
+    aws:eks_node_group:eks_node_group-0 -> aws:iam_role:eks_node_group-0-iam_role:
     aws:eks_node_group:eks_node_group-0 -> aws:subnet:vpc-0:subnet-0:
     aws:eks_node_group:eks_node_group-0 -> aws:subnet:vpc-0:subnet-1:
     kubernetes:service_account:eks_cluster-0:pod2 -> aws:eks_cluster:eks_cluster-0:
@@ -649,39 +649,37 @@ edges:
     aws:eks_cluster:eks_cluster-0 -> aws:subnet:vpc-0:subnet-1:
     aws:eks_cluster:eks_cluster-0 -> aws:vpc:vpc-0:
     aws:subnet:vpc-0:subnet-0 -> aws:availability_zone:region-0:availability_zone-0:
-    aws:subnet:vpc-0:subnet-0 -> aws:route_table_association:subnet-0-route_table-subnet-0-0:
-    aws:subnet:vpc-0:subnet-0 -> aws:security_group:vpc-0:security_group-eks_cluster-0-0:
+    aws:subnet:vpc-0:subnet-0 -> aws:route_table_association:subnet-0-subnet-0-route_table:
+    aws:subnet:vpc-0:subnet-0 -> aws:security_group:vpc-0:eks_cluster-0-security_group:
     aws:subnet:vpc-0:subnet-0 -> aws:vpc:vpc-0:
     aws:subnet:vpc-0:subnet-1 -> aws:availability_zone:region-0:availability_zone-1:
-    aws:subnet:vpc-0:subnet-1 -> aws:route_table_association:subnet-1-route_table-subnet-1-1:
-    aws:subnet:vpc-0:subnet-1 -> aws:security_group:vpc-0:security_group-eks_cluster-0-0:
+    aws:subnet:vpc-0:subnet-1 -> aws:route_table_association:subnet-1-subnet-1-route_table:
+    aws:subnet:vpc-0:subnet-1 -> aws:security_group:vpc-0:eks_cluster-0-security_group:
     aws:subnet:vpc-0:subnet-1 -> aws:vpc:vpc-0:
-    aws:route_table_association:subnet-0-route_table-subnet-0-0 -> aws:route_table:route_table-subnet-0-0:
-    aws:route_table_association:subnet-1-route_table-subnet-1-1 -> aws:route_table:route_table-subnet-1-1:
-    aws:security_group:vpc-0:security_group-eks_cluster-0-0 -> aws:eks_cluster:eks_cluster-0:
-    aws:security_group:vpc-0:security_group-eks_cluster-0-0 -> aws:vpc:vpc-0:
-    aws:route_table:route_table-subnet-0-0 -> aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0:
-    aws:route_table:route_table-subnet-0-0 -> aws:vpc:vpc-0:
-    aws:route_table:route_table-subnet-1-1 -> aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1:
-    aws:route_table:route_table-subnet-1-1 -> aws:vpc:vpc-0:
-    ? aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0 -> aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0
-    :
-    aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0 -> aws:subnet:vpc-0:subnet-2:
-    ? aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1 -> aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1
-    :
-    aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1 -> aws:subnet:vpc-0:subnet-3:
+    aws:route_table_association:subnet-0-subnet-0-route_table -> aws:route_table:subnet-0-route_table:
+    aws:route_table_association:subnet-1-subnet-1-route_table -> aws:route_table:subnet-1-route_table:
+    aws:security_group:vpc-0:eks_cluster-0-security_group -> aws:eks_cluster:eks_cluster-0:
+    aws:security_group:vpc-0:eks_cluster-0-security_group -> aws:vpc:vpc-0:
+    aws:route_table:subnet-0-route_table -> aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway:
+    aws:route_table:subnet-0-route_table -> aws:vpc:vpc-0:
+    aws:route_table:subnet-1-route_table -> aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway:
+    aws:route_table:subnet-1-route_table -> aws:vpc:vpc-0:
+    aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway -> aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip:
+    aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway -> aws:subnet:vpc-0:subnet-2:
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway -> aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip:
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway -> aws:subnet:vpc-0:subnet-3:
     aws:subnet:vpc-0:subnet-2 -> aws:availability_zone:region-0:availability_zone-0:
-    aws:subnet:vpc-0:subnet-2 -> aws:route_table_association:subnet-2-route_table-subnet-2-2:
+    aws:subnet:vpc-0:subnet-2 -> aws:route_table_association:subnet-2-subnet-2-route_table:
     aws:subnet:vpc-0:subnet-2 -> aws:vpc:vpc-0:
     aws:subnet:vpc-0:subnet-3 -> aws:availability_zone:region-0:availability_zone-1:
-    aws:subnet:vpc-0:subnet-3 -> aws:route_table_association:subnet-3-route_table-subnet-3-3:
+    aws:subnet:vpc-0:subnet-3 -> aws:route_table_association:subnet-3-subnet-3-route_table:
     aws:subnet:vpc-0:subnet-3 -> aws:vpc:vpc-0:
     aws:availability_zone:region-0:availability_zone-0 -> aws:region:region-0:
-    aws:route_table_association:subnet-2-route_table-subnet-2-2 -> aws:route_table:route_table-subnet-2-2:
+    aws:route_table_association:subnet-2-subnet-2-route_table -> aws:route_table:subnet-2-route_table:
     aws:availability_zone:region-0:availability_zone-1 -> aws:region:region-0:
-    aws:route_table_association:subnet-3-route_table-subnet-3-3 -> aws:route_table:route_table-subnet-3-3:
-    aws:route_table:route_table-subnet-2-2 -> aws:internet_gateway:vpc-0:internet_gateway-0:
-    aws:route_table:route_table-subnet-2-2 -> aws:vpc:vpc-0:
-    aws:route_table:route_table-subnet-3-3 -> aws:internet_gateway:vpc-0:internet_gateway-0:
-    aws:route_table:route_table-subnet-3-3 -> aws:vpc:vpc-0:
+    aws:route_table_association:subnet-3-subnet-3-route_table -> aws:route_table:subnet-3-route_table:
+    aws:route_table:subnet-2-route_table -> aws:internet_gateway:vpc-0:internet_gateway-0:
+    aws:route_table:subnet-2-route_table -> aws:vpc:vpc-0:
+    aws:route_table:subnet-3-route_table -> aws:internet_gateway:vpc-0:internet_gateway-0:
+    aws:route_table:subnet-3-route_table -> aws:vpc:vpc-0:
     aws:internet_gateway:vpc-0:internet_gateway-0 -> aws:vpc:vpc-0:
diff --git a/pkg/engine2/testdata/k8s_api.iac-viz.yaml b/pkg/engine2/testdata/k8s_api.iac-viz.yaml
index 7800756c..30e80855 100755
--- a/pkg/engine2/testdata/k8s_api.iac-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.iac-viz.yaml
@@ -7,8 +7,8 @@ resources:
   aws:availability_zone:region-0/availability_zone-0:
   aws:availability_zone:region-0/availability_zone-0 -> region/region-0:
 
-  aws:security_group:vpc-0/security_group-eks_cluster-0-0:
-  aws:security_group:vpc-0/security_group-eks_cluster-0-0 -> vpc/vpc-0:
+  aws:security_group:vpc-0/eks_cluster-0-security_group:
+  aws:security_group:vpc-0/eks_cluster-0-security_group -> vpc/vpc-0:
 
   aws:availability_zone:region-0/availability_zone-1:
   aws:availability_zone:region-0/availability_zone-1 -> region/region-0:
@@ -17,17 +17,17 @@ resources:
 
   aws:subnet:vpc-0/subnet-0:
   aws:subnet:vpc-0/subnet-0 -> aws:availability_zone:region-0/availability_zone-0:
-  aws:subnet:vpc-0/subnet-0 -> aws:security_group:vpc-0/security_group-eks_cluster-0-0:
+  aws:subnet:vpc-0/subnet-0 -> aws:security_group:vpc-0/eks_cluster-0-security_group:
   aws:subnet:vpc-0/subnet-0 -> vpc/vpc-0:
 
   aws:subnet:vpc-0/subnet-1:
   aws:subnet:vpc-0/subnet-1 -> aws:availability_zone:region-0/availability_zone-1:
-  aws:subnet:vpc-0/subnet-1 -> aws:security_group:vpc-0/security_group-eks_cluster-0-0:
+  aws:subnet:vpc-0/subnet-1 -> aws:security_group:vpc-0/eks_cluster-0-security_group:
   aws:subnet:vpc-0/subnet-1 -> vpc/vpc-0:
 
   eks_cluster/eks_cluster-0:
   eks_cluster/eks_cluster-0 -> iam_role/clusterrole-eks_cluster-0:
-  eks_cluster/eks_cluster-0 -> aws:security_group:vpc-0/security_group-eks_cluster-0-0:
+  eks_cluster/eks_cluster-0 -> aws:security_group:vpc-0/eks_cluster-0-security_group:
   eks_cluster/eks_cluster-0 -> aws:subnet:vpc-0/subnet-0:
   eks_cluster/eks_cluster-0 -> aws:subnet:vpc-0/subnet-1:
   eks_cluster/eks_cluster-0 -> vpc/vpc-0:
@@ -40,7 +40,7 @@ resources:
 
   ecr_repo/ecr_repo-0:
 
-  iam_role/iam_role-eks_node_group-0-2:
+  iam_role/eks_node_group-0-iam_role:
 
   iam_role/pod2:
   iam_role/pod2 -> iam_oidc_provider/eks_cluster-0:
@@ -55,12 +55,12 @@ resources:
   iam_role/aws-load-balancer-controller:
   iam_role/aws-load-balancer-controller -> iam_oidc_provider/eks_cluster-0:
 
-  ecr_image/ecr_image-pod2-0:
-  ecr_image/ecr_image-pod2-0 -> ecr_repo/ecr_repo-0:
+  ecr_image/pod2-ecr_image:
+  ecr_image/pod2-ecr_image -> ecr_repo/ecr_repo-0:
 
   eks_node_group/eks_node_group-0:
   eks_node_group/eks_node_group-0 -> eks_cluster/eks_cluster-0:
-  eks_node_group/eks_node_group-0 -> iam_role/iam_role-eks_node_group-0-2:
+  eks_node_group/eks_node_group-0 -> iam_role/eks_node_group-0-iam_role:
   eks_node_group/eks_node_group-0 -> aws:subnet:vpc-0/subnet-0:
   eks_node_group/eks_node_group-0 -> aws:subnet:vpc-0/subnet-1:
 
@@ -68,13 +68,13 @@ resources:
   kubernetes:service_account:eks_cluster-0/pod2 -> eks_cluster/eks_cluster-0:
   kubernetes:service_account:eks_cluster-0/pod2 -> iam_role/pod2:
 
-  elastic_ip/elastic_ip-nat_gateway-route_table-subnet-1-1-1-1:
+  elastic_ip/subnet-1-route_table-nat_gateway-elastic_ip:
 
   aws:subnet:vpc-0/subnet-3:
   aws:subnet:vpc-0/subnet-3 -> aws:availability_zone:region-0/availability_zone-1:
   aws:subnet:vpc-0/subnet-3 -> vpc/vpc-0:
 
-  elastic_ip/elastic_ip-nat_gateway-route_table-subnet-0-0-0-0:
+  elastic_ip/subnet-0-route_table-nat_gateway-elastic_ip:
 
   aws:subnet:vpc-0/subnet-2:
   aws:subnet:vpc-0/subnet-2 -> aws:availability_zone:region-0/availability_zone-0:
@@ -92,7 +92,7 @@ resources:
   kubernetes:service_account:eks_cluster-0/aws-load-balancer-controller -> iam_role/aws-load-balancer-controller:
 
   kubernetes:pod:eks_cluster-0/pod2:
-  kubernetes:pod:eks_cluster-0/pod2 -> ecr_image/ecr_image-pod2-0:
+  kubernetes:pod:eks_cluster-0/pod2 -> ecr_image/pod2-ecr_image:
   kubernetes:pod:eks_cluster-0/pod2 -> eks_cluster/eks_cluster-0:
   kubernetes:pod:eks_cluster-0/pod2 -> eks_node_group/eks_node_group-0:
   kubernetes:pod:eks_cluster-0/pod2 -> kubernetes:service_account:eks_cluster-0/pod2:
@@ -103,13 +103,13 @@ resources:
   aws:internet_gateway:vpc-0/internet_gateway-0:
   aws:internet_gateway:vpc-0/internet_gateway-0 -> vpc/vpc-0:
 
-  aws:nat_gateway:subnet-3/nat_gateway-route_table-subnet-1-1-1:
-  aws:nat_gateway:subnet-3/nat_gateway-route_table-subnet-1-1-1 -> elastic_ip/elastic_ip-nat_gateway-route_table-subnet-1-1-1-1:
-  aws:nat_gateway:subnet-3/nat_gateway-route_table-subnet-1-1-1 -> aws:subnet:vpc-0/subnet-3:
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway:
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway -> elastic_ip/subnet-1-route_table-nat_gateway-elastic_ip:
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway -> aws:subnet:vpc-0/subnet-3:
 
-  aws:nat_gateway:subnet-2/nat_gateway-route_table-subnet-0-0-0:
-  aws:nat_gateway:subnet-2/nat_gateway-route_table-subnet-0-0-0 -> elastic_ip/elastic_ip-nat_gateway-route_table-subnet-0-0-0-0:
-  aws:nat_gateway:subnet-2/nat_gateway-route_table-subnet-0-0-0 -> aws:subnet:vpc-0/subnet-2:
+  aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway:
+  aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway -> elastic_ip/subnet-0-route_table-nat_gateway-elastic_ip:
+  aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway -> aws:subnet:vpc-0/subnet-2:
 
   aws:api_integration:rest_api_4/rest_api_4_integration_0:
   aws:api_integration:rest_api_4/rest_api_4_integration_0 -> aws:api_method:rest_api_4/rest_api_4_integration_0_method:
@@ -132,21 +132,21 @@ resources:
   kubernetes:config_map/fluent-bit-cluster-info -> eks_cluster/eks_cluster-0:
   kubernetes:config_map/fluent-bit-cluster-info -> kubernetes:namespace/amazon-cloudwatch:
 
-  route_table/route_table-subnet-3-3:
-  route_table/route_table-subnet-3-3 -> aws:internet_gateway:vpc-0/internet_gateway-0:
-  route_table/route_table-subnet-3-3 -> vpc/vpc-0:
+  route_table/subnet-3-route_table:
+  route_table/subnet-3-route_table -> aws:internet_gateway:vpc-0/internet_gateway-0:
+  route_table/subnet-3-route_table -> vpc/vpc-0:
 
-  route_table/route_table-subnet-2-2:
-  route_table/route_table-subnet-2-2 -> aws:internet_gateway:vpc-0/internet_gateway-0:
-  route_table/route_table-subnet-2-2 -> vpc/vpc-0:
+  route_table/subnet-2-route_table:
+  route_table/subnet-2-route_table -> aws:internet_gateway:vpc-0/internet_gateway-0:
+  route_table/subnet-2-route_table -> vpc/vpc-0:
 
-  route_table/route_table-subnet-1-1:
-  route_table/route_table-subnet-1-1 -> aws:nat_gateway:subnet-3/nat_gateway-route_table-subnet-1-1-1:
-  route_table/route_table-subnet-1-1 -> vpc/vpc-0:
+  route_table/subnet-1-route_table:
+  route_table/subnet-1-route_table -> aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway:
+  route_table/subnet-1-route_table -> vpc/vpc-0:
 
-  route_table/route_table-subnet-0-0:
-  route_table/route_table-subnet-0-0 -> aws:nat_gateway:subnet-2/nat_gateway-route_table-subnet-0-0-0:
-  route_table/route_table-subnet-0-0 -> vpc/vpc-0:
+  route_table/subnet-0-route_table:
+  route_table/subnet-0-route_table -> aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway:
+  route_table/subnet-0-route_table -> vpc/vpc-0:
 
   iam_policy/iam_policy-0:
 
@@ -165,8 +165,8 @@ resources:
   kubernetes:manifest/fluent-bit -> eks_cluster/eks_cluster-0:
   kubernetes:manifest/fluent-bit -> kubernetes:config_map/fluent-bit-cluster-info:
 
-  kubernetes:kube_config/kube_config-eks_cluster-0-0:
-  kubernetes:kube_config/kube_config-eks_cluster-0-0 -> eks_cluster/eks_cluster-0:
+  kubernetes:kube_config/eks_cluster-0-kube_config:
+  kubernetes:kube_config/eks_cluster-0-kube_config -> eks_cluster/eks_cluster-0:
 
   kubernetes:helm_chart:eks_cluster-0/metricsserver:
   kubernetes:helm_chart:eks_cluster-0/metricsserver -> eks_cluster/eks_cluster-0:
@@ -175,21 +175,21 @@ resources:
   security_group_rule/security_group_rule-0:
   security_group_rule/security_group_rule-0 -> vpc/vpc-0:
 
-  route_table_association/subnet-3-route_table-subnet-3-3:
-  route_table_association/subnet-3-route_table-subnet-3-3 -> route_table/route_table-subnet-3-3:
-  route_table_association/subnet-3-route_table-subnet-3-3 -> aws:subnet:vpc-0/subnet-3:
+  route_table_association/subnet-3-subnet-3-route_table:
+  route_table_association/subnet-3-subnet-3-route_table -> route_table/subnet-3-route_table:
+  route_table_association/subnet-3-subnet-3-route_table -> aws:subnet:vpc-0/subnet-3:
 
-  route_table_association/subnet-2-route_table-subnet-2-2:
-  route_table_association/subnet-2-route_table-subnet-2-2 -> route_table/route_table-subnet-2-2:
-  route_table_association/subnet-2-route_table-subnet-2-2 -> aws:subnet:vpc-0/subnet-2:
+  route_table_association/subnet-2-subnet-2-route_table:
+  route_table_association/subnet-2-subnet-2-route_table -> route_table/subnet-2-route_table:
+  route_table_association/subnet-2-subnet-2-route_table -> aws:subnet:vpc-0/subnet-2:
 
-  route_table_association/subnet-1-route_table-subnet-1-1:
-  route_table_association/subnet-1-route_table-subnet-1-1 -> route_table/route_table-subnet-1-1:
-  route_table_association/subnet-1-route_table-subnet-1-1 -> aws:subnet:vpc-0/subnet-1:
+  route_table_association/subnet-1-subnet-1-route_table:
+  route_table_association/subnet-1-subnet-1-route_table -> route_table/subnet-1-route_table:
+  route_table_association/subnet-1-subnet-1-route_table -> aws:subnet:vpc-0/subnet-1:
 
-  route_table_association/subnet-0-route_table-subnet-0-0:
-  route_table_association/subnet-0-route_table-subnet-0-0 -> route_table/route_table-subnet-0-0:
-  route_table_association/subnet-0-route_table-subnet-0-0 -> aws:subnet:vpc-0/subnet-0:
+  route_table_association/subnet-0-subnet-0-route_table:
+  route_table_association/subnet-0-subnet-0-route_table -> route_table/subnet-0-route_table:
+  route_table_association/subnet-0-subnet-0-route_table -> aws:subnet:vpc-0/subnet-0:
 
   load_balancer_listener/rest_api_4_integration_0-pod2:
   load_balancer_listener/rest_api_4_integration_0-pod2 -> load_balancer/rest-api-4-integbcc77100:
diff --git a/pkg/knowledge_base2/edge_template_test.go b/pkg/knowledge_base2/edge_template_test.go
new file mode 100644
index 00000000..30cb1629
--- /dev/null
+++ b/pkg/knowledge_base2/edge_template_test.go
@@ -0,0 +1,139 @@
+package knowledgebase2
+
+import (
+	"testing"
+
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/construct2/graphtest"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestUnique_CanAdd(t *testing.T) {
+	a := graphtest.ParseId(t, "p:source:a")
+	b := graphtest.ParseId(t, "p:target:b")
+
+	tests := []struct {
+		name   string
+		unique Unique
+		graph  []any
+		want   bool
+	}{
+		{
+			name:   "not unique",
+			unique: Unique{},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+				"p:source:a -> p:target:b",
+			},
+			want: true,
+		},
+		{
+			name:   "one-to-one (no match)",
+			unique: Unique{Source: true, Target: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+			},
+			want: true,
+		},
+		{
+			name:   "one-to-one (match target)",
+			unique: Unique{Source: true, Target: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+				"p:source:a -> p:target:x",
+			},
+			want: false,
+		},
+		{
+			name:   "one-to-one (match target)",
+			unique: Unique{Source: true, Target: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+				"p:source:x -> p:target:b",
+			},
+			want: false,
+		},
+		{
+			name:   "one-to-one (full match)",
+			unique: Unique{Source: true, Target: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+				"p:source:a -> p:target:b",
+			},
+			want: true,
+		},
+		{
+			name:   "one-to-many (no match)",
+			unique: Unique{Source: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+			},
+			want: true,
+		},
+		{
+			name:   "one-to-many (match target)",
+			unique: Unique{Source: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+				"p:source:a -> p:target:x",
+			},
+			want: true,
+		},
+		{
+			name:   "one-to-many (match source)",
+			unique: Unique{Source: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+				"p:source:x -> p:target:b",
+			},
+			want: false,
+		},
+		{
+			name:   "many-to-one (no match)",
+			unique: Unique{Target: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+			},
+			want: true,
+		},
+		{
+			name:   "many-to-one (match target)",
+			unique: Unique{Target: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+				"p:source:a -> p:target:x",
+			},
+			want: false,
+		},
+		{
+			name:   "many-to-one (match source)",
+			unique: Unique{Target: true},
+			graph: []any{
+				"p:source:a",
+				"p:target:b",
+				"p:source:x -> p:target:b",
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			graph := graphtest.MakeGraph(t, construct.NewGraph(), tt.graph...)
+			edges, err := graph.Edges()
+			require.NoError(t, err)
+			got := tt.unique.CanAdd(edges, a, b)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}