Skip to main content

Shared VPC Subnet tagging

This post was previously published on https://www.lionsville.nl/blog/shared-vpc-subnet-tagging. Since it is no longer there, it is re-published on this site.

One of the services we provide for our customers is the deployment and management of an AWS Landing Zone. Part of the Landing Zone is a centralized networking environment that is used as the centralized VPC for ingress and egress network traffic. VPC sharing is used, implementation varies, for this article we implement a shared VPC per AWS account with 3 subnets per VPC.

"High level overview"

VPC Sharing overview

Each AWS Account gets its own VPC with 3 type of subnets:

With 1 subnet per Availability Zone, a total of 9 subnets are created. The subnets are then shared with Resource Access Manager (RAM) with the Team account.

For the sake of simplicity, this article will only focus on the subnet sharing part of this implementation. One of the big downsides of RAM is that the shared resource in the Team acccount does not inherit tags. Especially for implementations like EKS or other AWS services that rely on tagging, this is a problem. As part of the VPC deployment in the Shared Network account, several tags are created of which a few of them we would like to make available in the Team account. The tags that are discussed in this post are Name, AccountID, subnetType and aws-cdk:subnet-type.

"VPC Sharing overview"

Process

  1. EventBridge detects an AssociateResourceShare Cloudwatch Event.
  2. EventBridge triggers the Lambda function with the Cloudwatch event json as input.
  3. The Lambda function looks up the subnet-id that is in the body of the Cloudwatch event and grabs the tags that are configured on that subnet.
  4. The Lambda function assumes a role in the team account.
  5. The Lambda function updates the tags on the subnets in the team account.

The process in the will be discussed in more detail, using code examples (Terraform and Lambda). The code can be found here.

If you want Eventbridge to be able to record events with a detail-type value of AWS API Call via CloudTrail, a CloudTrail trail with logging enabled is required. The event that Eventbridge is configured to look for is the AssociateResourceShare event. Check the API Documentation for the different events that are available for RAM. The target for the event is a Lambda function that was created by Markus Muhr. Two resources are created, an event rule and an event target.

resource "aws_cloudwatch_event_rule" "ram" {
  name        = "capture-ram-creation"
  description = "Capture creation of RAM shares"

  event_pattern = jsonencode({
    "source" : ["aws.ram"],
    "detail-type" : ["AWS API Call via CloudTrail"],
    "detail" : {
      "eventSource" : ["ram.amazonaws.com"],
      "eventName" : ["AssociateResourceShare"]
    }
  })
}

resource "aws_cloudwatch_event_target" "lambda" {
  target_id = "lambda-function-target"
  rule      = aws_cloudwatch_event_rule.ram.name
  arn       = aws_lambda_alias.tagger_alias.arn
}

Lambda function

The Lambda function contains several functions:

The example shows a simplified version of the actual function that was implemented, it should however be easy to extend upon this code.

IAM role in Team account(s)

The team account requires an IAM role to be created that can be assumed by the Lambda function in order to create the tags. This is a very simple role, which is created using org-formation In short, in order to use Terraform several resources like an S3 bucket for state backend and IAM roles are required. Org-formation uses Cloudformation under the hood for creating the resources that are required for Terraform and resources that need to be created in all accounts. When AWS Control Tower is not an option, we leverage org-formation as our account vending machine in AWS. Note the OrganizationBinding parameter, this is specific to org-formation to target or filter only specific accounts or OU's. If you don't use org-formation, omit this parameter.

LambdaTaggerPolicy:
  Type: "AWS::IAM::ManagedPolicy"
  OrganizationBinding: !Ref TeamAccountBinding
  Properties:
    ManagedPolicyName: LambdaTaggerPolicy
    Description: Policy to allow access from lz-net Lambda function
    PolicyDocument:
      Version: 2012-10-17
      Statement:
        - Effect: Allow
          Action:
            - "ec2:CreateTags"
          Resource:
            - "arn:aws:ec2:*:*:subnet/*"
            - "arn:aws:ec2:*:*:vpc/*"
    Roles:
      - !Ref LambdaTaggerRole

LambdaTaggerRole:
  Type: AWS::IAM::Role
  OrganizationBinding: !Ref TeamAccountBinding
  Properties:
    RoleName: LambdaTaggerRole
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Principal:
            Service:
              - lambda.amazonaws.com
          Action:
            - "sts:AssumeRole"
        - Effect: Allow
          Principal:
            AWS:
              - "<central network account ID>"
          Action:
            - "sts:AssumeRole"

That's it! Even though the current Lambda is “only” used to tag resources, you can imagine this concept can be used for all sorts of implementations. Go nuts!