0

I need to add a condition (e.g. isProd or isSpecificRegion) to all/multiple of my resources in CDK. I saw that I can add a CfnCondition to a specific CDK resources using this technique: Add Conditions to Resources in CDK. e.g.

// Create the user using the L2 construct
const user = new iam.User(this, 'User');

// Add the condition on the underlying AWS::IAM::User
(user.node.defaultChild as iam.CfnUser).cfnOptions.condition = regionCondition

Is there a way to iterate over all resources and apply a condition?

or9ob
  • 2,313
  • 4
  • 25
  • 45

3 Answers3

1

Generally, you don't want to use the CfnConditional with CDK unless there is a very specific need. As Amit mentioned in their comment, the idiomatic CDK appraoch would be to use the language's if/else logic to either instantiate a construct or not.

If you absolutely need to use CFNConditional on every Resource, I think that the aspects feature could apply the conditional to the underlying cloudformation resources using escape hatches, though I would strongly advise against this.

  • Thanks - I found out a couple of ways of getting this done (see my responses here), and eventually settled on using an Aspect: https://stackoverflow.com/a/61236670/117750 – or9ob Apr 15 '20 at 19:06
0

I figured out a way to automatically apply a CfnCondition to every resource in the stack using the Construct#onPrepare hook in the Stack

From the above page, Construct#onPrepare() is:

Perform final modifications before synthesis

This method can be implemented by derived constructs in order to perform final changes before synthesis. prepare() will be called after child constructs have been prepared.

This is an advanced framework feature. Only use this if you understand the implications.

The following CDK code (example code - I only put in the relevant bits here) is in Kotlin but would apply to any supported CDK language:

class MyStack internal constructor(app: App, name: String) : Stack(app, name) {
      private lateinit var myCondition: CfnCondition

    init {
        myCondition = CfnCondition.Builder.create(this, "myCondition")
                .expression(Fn.condition...) // Fill in your condition
                .build()
    }

    /**
     * Use the `Stack#onPrepare` hook to find all CF resources and apply our stack standard CF condition on them.
     * See:
     * * [Stack.onPrepare]: https://docs.aws.amazon.com/cdk/api/latest/typescript/api/core/construct.html#core_Construct_onPrepare
     */
    override fun onPrepare() {
        super.onPrepare()
        findAllCfnResources(this.node).forEach { it.cfnOptions.condition = this.myCondition }
    }

    /**
     * Recurse through all children nodes and accumulate [CfnResource] nodes.
     */
    private fun findAllCfnResources(node: ConstructNode): List<CfnResource> {
        val nodes = node.children.map { it.node } + node
        val cfnResources: List<CfnResource> = nodes.flatMap { it.findAll() }.mapNotNull { it as? CfnResource }

        if (node.children.isEmpty()) {
            return cfnResources
        }

        return node.children.fold(cfnResources) {
            accumulatedCfnResources, it -> accumulatedCfnResources + findAllCfnResources(it.node)
        }
    }
}

A unit-test (Junit and AssertJ, also in Kotlin) I added verifies that this covers all resources:

private val mapper = ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)

private lateinit var stack: Stack
private lateinit var synthesizedStackCfTemplate: JsonNode

@BeforeEach
fun setUp() {
    val app = App()
    stack = MyStack(app, "MyUnitTestStack")
    synthesizedStackCfTemplate = mapper.valueToTree(app.synth().getStackArtifact(stack.artifactId).template)
}

@Test
fun `All CfnResources have the expected CF Condition`() {
    val expectedCondition = "myCondition"

    val softly = SoftAssertions()
    synthesizedStackCfTemplate.get("Resources").forEach { resource ->
        softly.assertThat(resource.get("Condition")?.asText())
                .describedAs("Resource is missing expected CF condition [$expectedCondition]: $resource")
                .isEqualTo(expectedCondition)
    }
    softly.assertAll()
}
or9ob
  • 2,313
  • 4
  • 25
  • 45
0

Using CDK Aspects (as suggested by Richard) I was able to simply my solution even further (building upon my previous solution: https://stackoverflow.com/a/61234581/117750):

class ApplyCfnConditionAspect(val cfnCondition: CfnCondition): IAspect {
    override fun visit(construct: IConstruct) {
        (construct as? CfnResource)?.cfnOptions?.condition = this.cfnCondition
    }
}

... and then within the Stack, after specifying all the resources:

this.node.applyAspect(ApplyCfnConditionAspect(shouldEnableAnalyticsCondition))
or9ob
  • 2,313
  • 4
  • 25
  • 45
  • 1
    the applyAspect code seems to run after your code has executed, so you can add it first or last in your stack - you will still apply the aspect to all nodes. – jesper Apr 20 '20 at 17:39