Abstracting CloudFormation IAM with Nested Stacks

Once we started extracting applications into different logical CloudFormation stacks and physical templates, we began to notice quite a lot of duplication in our json when it came to declaring IAM rules. Some of our projects store their puppet, hiera and rpm files in restricted S3 buckets so allowing stacks access to them based upon environment, region, stack name and other criteria quickly becomes quite long-winded. After looking at a couple of dozen application templates and finding that over 30% of the json was IAM based it was time to find a different approach.

One of the CloudFormation techniques I’d seen mentioned but never used before was nested CloudFormation stacks. This allows you to define an entire stack as just another resource in your template. Here’s some example json that does this:

  "Resources" : {

    "IAMRolesStack" : {
      "Type" : "AWS::CloudFormation::Stack",
      "Properties" : {
        "TemplateURL" : "https://s3-eu-west-1.amazonaws.com/my-iam-rules/projectname/iam-roles-20140301.json",
        "Parameters" : {
          "Stack": "testy-webapp",
          "Type":  "webapp",
          "App":   "tinyess",
          "Env":   { "Ref" : "DeploymentEnvironment" }
        }
      }
    }

  }

You can see that a stack is declared in the same manner as all other resources. The ‘TemplateURL’ property must point to a URL that hosts a complete, valid CloudFormation template. This allows you to develop the nested stack in the same way as you’d progress your actual application templates and test it in isolation. For my experiments I found it easiest to store them in S3 under a basic hierarchy with a little versioning to allow multiple versions of the IAM rules to be in use at once across the stacks. The other properties in the example are ‘Parameters’. These are passed to the sub-stack at creation time as actual parameters and are what makes this approach so flexible and powerful.

Inside the nested stack template we add define a AWS::IAM::Role, an AWS::IAM::InstanceProfile and a number of AWS::IAM::Policy types that are abstracted to only allow access for one app/environment combination at a time. We do this using the parameters we pass in as values at different levels of the hierarchy. This way we can ensure that every application using a specific version of the IAM roles gets exactly the same permissions while not bulk pasting it into each applications json template or hard coding any of the application specific values. It’s also worth noting that as stacks are given “CloudFormationed” IDs that include some randomness you can have multiple versions of the nested stack at once with no overlap or conflicts between apps.

You can see a small extract from our sample IAM template, with the parameters interpolated into the path, here -

  "SecretPolicy": {
    "Type": "AWS::IAM::Policy",
    "Properties": {
      "PolicyDocument": {
        "Statement": [ {
            "Effect": "Allow",
            "Action": [
              "S3:ListBucket"
            ],
            "Resource": [
              "arn:aws:s3:::org.example.test.secrets"
            ]
          },
          {
            "Effect": "Allow",
            "Action": [
              "S3:GetObject"
            ],
            "Resource": [
              "arn:aws:s3:::org.example.test.secrets/common.yaml",
              { "Fn::Join" : [ "", [
                "arn:aws:s3:::org.example.test.secrets/type/",
                { "Ref" : "App"   }, ".",
                { "Ref" : "Type"  }, ".",
                { "Ref" : "Env"   }, ".",
                { "Ref" : "Stack" }, ".yaml"
              ] ] },

              { "Fn::Join" : [ "", [
                "arn:aws:s3:::org.example.test.secrets/type/",
                { "Ref" : "App"  }, ".",
                { "Ref" : "Type" }, ".yaml"
              ] ] },

Now that we’ve declared and created the nested stack let’s use the IamInstanceProfile it created in the auto scaling launch configuration that lives in the containing stack.

    "AppServerFleetLaunchConfig" : {
      "Type" : "AWS::AutoScaling::LaunchConfiguration",
      "Properties" : {
        ...
        "IamInstanceProfile": { "Fn::GetAtt" : [ "IAMRolesStack", "Outputs.InstanceProfile" ] },
        ...
      }
    }

Accessing nested stack outputs is as simple as a call to Fn::GetAtt with the resource name of the nested stack as the first argument (IAMRolesStack as seen in our first code snippet) and the outputs name as part of the second.

So what did we get from this? A few very worth while things. We removed a LOT of boilerplate from all our application templates. This also makes CloudFormation application templates easier to create as only a few people need in-depth knowledge of our IAM rules and bucketing scheme, application templates can focus on the application. It’s easier to confirm that applications have the same access rights based on the S3 bucket used, rather than diffing through lots of subtly different IAM resources.

I’m using this technique on a couple of medium size projects at the moment and so far it seems like a good way to overcome IAM json spaghetti with no large drawbacks.