Containers

How to use components to augment the infrastructure in an AWS Proton template

Introduction

AWS Proton is a managed service that helps platform engineers scale their impact by defining, vending, and maintaining infrastructure templates for self-service deployments. With Proton, customers can standardize centralized templates to meet security, cost, and compliance goals. Proton helps platform engineers scale up their impact with a self-service model, resulting in higher velocity for the development and deployment process throughout an application lifecycle.

In Proton, platform engineers write service templates to encode their organization’s infrastructure and CI/CD standards, extracting configurable parameters in an input schema. Developers then use Proton as a self-service interface, selecting from these templates and providing the required inputs. Proton then renders an Infrastructure-as-Code (IaC) template and directly provisions the infrastructure through AWS CloudFormation or makes a pull request to a repository that customers can use to deploy using Terraform.

As the complexity and number of applications grows, it becomes challenging to manage diverging application-specific requirements with a small number of centralized service templates. Specific development teams might need to complement a particular template with a few additional resources. To accommodate these requirements, platform teams would either have to manage many service templates with small variations or maintain increasingly complex input schemas which change the behavior of a shared template.

 Move faster and scale bigger

To move faster and scale, platform engineers want to help development teams get the additional resources they need within guard rails defined by the platform engineering team. For example, a platform engineering team may create a web service template, and then look for a way to allow application developers to add a storage solution to it that works for their service, such as an S3 bucket or DynamoDB table— without having to incorporate all options to their template or needing to create two different versions of the template, depending on the resource.

Components are a new type of Proton resource that provides additional flexibility to platform teams, a mechanism to extend core infrastructure, and define guard rails that allow developers to manage aspects of their applications’ infrastructure. Components allow service templates to be extended—the service template will continue to define the majority of the core application infrastructure—while developers can provide additional infrastructure required by their application. Development teams do not need to understand or modify the whole service template and can focus on managing the infrastructure they require.

Infrastructure deployments with components

Let’s talk about how components work. In Proton, infrastructure is broken into environment and services. Proton environments are shared resources shared by multiple service instances, such as a Virtual Private Cloud (VPC) or an Amazon Elastic Container Service (ECS) cluster. They are typically created by platform engineers. As part of creating the environments, Proton stores and exposes provisioning outputs, such as the VPC ID or cluster name, that will be relevant later. Services and service instances are instantiations of service templates that define application infrastructure, such as a Fargate web service, and are typically created by developers. When a developer creates a service instance, they select which environment it will be deployed to. The provisioning outputs of the selected environment are used as input parameters to provision the service instance resources (for example, the Fargate web service will be deployed to the corresponding ECS cluster).

Architecture showing user inputs

Components are infrastructure stacks, defined by developers, which may export additional outputs that can be consumed by associated service instances.

A component is created by writing a CloudFormation or Terraform IaC file, which specifies the resources to provision, and providing it directly to Proton. This is different from other Proton resources which are created as instances of versioned Proton templates written by platform engineers. Components allow for quick iteration by removing the need for managing template versions and publishing.

When the component is created, developers can specify a service instance to associate with it. The component IaC file may export resource names or identifiers. These component outputs will be provided to the associated service instance template when it is rendered. See details of how inputs and parameters can be used in componets in our documentation here.

To incorporate components into a service, platform engineers update their service template to properly read and use the component outputs. Once a service template has been updated to be compatible with components, platform engineers can indicate this on the service template version’s supported component sources field. To make use of components, developers output the information required (such as the name or Amazon Resource Name (ARN) of the resource) to make the component interact with the service instance. Platform engineers should share details about which types of resources they can support and what outputs they require with their development teams.

Although components are typically created to support a specific service instance, you can also choose to disassociate them from the instance they are linked to. This is useful if, for example, you want to delete a service instance but keep a storage resource that you associated with it and link it to a new instance, preserving the data for continued use.

Example: extending a CloudFormation Service Template with components

The Proton sample template repository (available for CloudFormation and Terraform) provides a load balanced Fargate service template example (also available for CloudFormation and Terraform). The service template defines the complete infrastructure required for networking, monitoring and scaling the Fargate service. In this blog post, we are going to use the CloudFormation version of the template. If you prefer to read a Terraform example, you can find the details in the components folder in our sample repositories (CloudFormation and Terraform).

AWS Fargate is a versatile compute platform and, in practice, you may want to use the same template for many applications with slightly different infrastructure requirements. For example, one application may require an S3 bucket for large object storage, another might require an SQS Queue for asynchronous communication, while another may require an Amazon DynamoDB table for transaction processing.

The additional infrastructure can be provisioned by a component. In this example, the developer wants to add an S3 bucket, which means that the component will have two resources: an S3 bucket, and an IAM Managed Policy for accessing the new bucket. ARNs for these resources will be exported by the component as outputs which will be incorporated into the service template, so that the Fargate task can access the bucket.

As part of this exercise, we are going to make the following changes:

  • Define the component: As a developer, we will write the CloudFormation template for an S3 bucket, and output the required values to allow our Fargate task to interact with it.
  • Update the service template to support components: As a platform engineer, we will introduce changes to our service template to consume the output generated by the component and adapt the task. These changes are: (a) passing the S3 bucket name as an environment variable; (b) updating the task execution role to give the task permission to access the S3 bucket
  • Provide the required permissions the environment level: As a platform engineer, we will create a new IAM role that Proton will use to provision the component
  • Limit the permissions that can be created in the component: As a platform engineer, we will use IAM Permissions Boundaries to ensure that we only allow developers to enable permissions to specific resources in their components.

Create the component

CloudFormation component IaC files can be defined using Jinja variables and parameters defined here, similar to how Proton environment and service templates are created.

The example template below shows a simple S3 bucket component. In the file, we create an S3 bucket (using AWS::S3::Bucket) and a policy to access it (using AWS::IAM::ManagedPolicy), and we output the bucket ARN and access policy in the outputs section. Note that, because we are going to attach this component to a service, we can refer to service instance parameters, such as environment name or service name.

# An S3 Component, which defines both the resource and a policy for accessing 
# the S3 bucket.
Resources:
  S3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: '{{environment.name}}-{{service.name}}-{{component.name}}'
  S3BucketAccessPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - 's3:Get*'
              - 's3:List*'
              - 's3:PutObject'
            Resource: !GetAtt S3Bucket.Arn
Outputs:
  BucketName:
    Description: "Bucket to access"
    Value: !GetAtt S3Bucket.Arn
  BucketAccessPolicyArn:
    Value: !Ref S3BucketAccessPolicy

Incorporate component outputs in the service template

In order to extend the load balanced Fargate service, we need to incorporate the component’s outputs to the service instance. For our example, this means two things: (1) passing the bucket name as an environment variable which can be read by the application code; and (2) extending the Fargate task’s IAM Role to access the S3 bucket. To make it easier to incorporate optional component outputs in service templates, Proton provides a number of new Jinja filters. Information on the built-in Jinja filters can be found here and on the Proton Jinja filters here.

First, we use the proton_cfn_ecs_task_definition_formatted_env_vars Jinja filter, shown below, to format the outputs of the component as ECS Task Definition environment variables. When the task runs, the outputs will be available as environment variables, which developers can refer to on their code to access the S3 bucket. We incorporate this into our task definition (see the sample template). By using a Jinja if/else statement, we ensure that the environment variable will only be added if there are components that use it.

ServiceTaskDef:
  Type: AWS::ECS::TaskDefinition
  Properties:
    ...
    ContainerDefinitions:
      - Name: '{{service_instance.name}}'
       ...
       {% if service_instance.components.default.outputs is defined %}
       Environment:
         {{ service_instance.components.default.outputs
             | proton_cfn_ecs_task_definition_formatted_env_vars }}
       {% endif %}

We also need to ensure that the Fargate task can access the resource. In our service template, we create an IAM role that ECS will use to access other AWS resources. As written in the template, it only has permissions to work with SNS (see the template). We are going to update the permissions to include the policy that we exported in the component outputs. The proton_cfn_iam_policy_arns filter will select IAM Managed Policy ARNs exported by the component and incorporate them in the task role, as shown below:

ServiceTaskDefTaskRole:
  Type: AWS::IAM::Role
  Properties:
    ...
    ManagedPolicyArns:
      - !Ref BaseTaskRoleManagedPolicy
      {{ service_instance.components.default.outputs
          | proton_cfn_iam_policy_arns }}

Note that we do not need an if/else statement here, since the template already contains a managed policy and the filter will resolve to an empty list if there are no components.

With this change, the task permissions will be expanded to include any managed policy defined in the component and passed as an output. As a platform engineering team, you probably want to create guard rails for the types of managed policies developers can export. For instance, you want to prevent developers from creating a managed policy with admin privileges and attaching it to an ECS task. You can control this using IAM Permissions Boundaries. Later in this blog post, in the section “Using IAM roles and IAM permission boundaries to control access,” we explain how you can use this feature.

When Proton renders the service template, it will include the outputs from the component. An example of the rendered service template is shown here:

Resources: 
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      TaskRoleArn: !Ref TaskRole
      ContainerDefinitions:
        - Name: "the-service-name"
          # ...
          Environment:
            - Name: BucketName
              Value: arn:aws:s3:us-east-1:123456789012:environment_name-service_name-service_instance_name-component_name
            - Name: BucketAccessPolicyArn
              Value: arn:aws:iam::123456789012:policy/cfn-generated-policy-name
  # ...

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      # ...
      ManagedPolicyArns:
        - !Ref BaseTaskRoleManagedPolicy
        - arn:aws:iam::123456789012:policy/cfn-generated-policy-name

Service instances need to operate without components so you can easily link and unlink components from them. For this reason, components should always be considered optional, and so the service template must be valid by itself and able to be provisioned without component outputs. Likewise, developers might want to configure their code to work without requiring the component outputs (for example, by using default values when no environment variables are available). That way, a service instance can be created and then the component added later.

Enable provisioning in the environment

To give platform engineers the ability to control what components are created, we have added a new component IAM role to the environment for CloudFormation-based infrastructure. This allows you to select a role for the resources in your service and environment templates, and a different one —potentially more restrictive— specific for components. This way, Proton can continue to use the same role and permissions you are already using for your Proton templates, and you get to control which granular resources can be provisioned as components.

The component IAM role has two sections: a first section gives Proton permissions to call CloudFormation APIs on your behalf, and it includes permissions to call CloudFormation APIs, including creating and deleting stacks. You can copy this section from below. The second section, specific to each use case, gives CloudFormation permissions to provision the specific resources that you want to allow as components. In this example, we will provide permissions to create S3 buckets and IAM policies:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:CancelUpdateStack",
                "cloudformation:CreateChangeSet",
                "cloudformation:DeleteChangeSet",
                "cloudformation:DescribeStacks",
                "cloudformation:ContinueUpdateRollback",
                "cloudformation:DetectStackResourceDrift",
                "cloudformation:DescribeStackResourceDrifts",
                "cloudformation:DescribeStackEvents",
                "cloudformation:CreateStack",
                "cloudformation:DeleteStack",
                "cloudformation:UpdateStack",
                "cloudformation:DescribeChangeSet",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:ListChangeSets",
                "cloudformation:ListStackResources"
            ],
            "Resource": "arn:aws:cloudformation:*:279245132597:stack/AWSProton-*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:CreateBucket",
                "s3:DeleteBucket",
                "s3:GetBucket",
                "iam:CreatePolicy",
                "iam:DeletePolicy",
                "iam:GetPolicy",
                "iam:ListPolicyVersions",
                "iam:DeletePolicyVersion"
            ],
            "Resource": "*",
            "Condition": {
                "ForAnyValue:StringEquals": {
                    "aws:CalledVia": "cloudformation.amazonaws.com"
                }
            }
        }
    ]
}

Using IAM roles and IAM permission boundaries to control access

We’ve mentioned a total of three IAM roles in this example. Let’s look at a summary of them:

Role How it works with CloudFormation provisioning How it works with Terraform provisioning What is it used for? How do we update it when we create a component?
Environment role Platform engineers provide it to Proton at environment creation Platform engineers configure it as part of their PR-based provisioning process Provision all the infrastructure resources in the environment and all the service instances inside of it Not changed
Component role Platform engineers provide it to Proton when enabling components in an environment Platform engineers configure it as part of their PR-based provisioning process Provision all the resources in any components in that environment Not changed
Task Definition Role CloudFormation creates it when provisioning the service instance infrastructure Terraform creates it when provisioning the service instance infrastructure Allows the ECS task to interact with other AWS resources We modify it by including a policy to enable interacting with the resources defined in the component

Before, we showed how we modified the Task Definition Role to access our newly created S3 bucket. In order to support components, platform engineers will want to allow developers to create the policies they need, but limit what permissions can be added to them—as mentioned above, platform engineers will want to make sure that developers cannot define an admin policy and attach it to their ECS task. IAM Permissions boundaries provide a way to do this.

Let’s see how this is done in our example. We are going to add one more resource to our service template: a permission boundary definition which declares the broadest permissions that can be given to this role. This only has to be done once, as part of enabling this template to use components.

 ServiceTaskDefTaskRole:
    Type: 'AWS::IAM::Role'
    Properties:
       ...
       PermissionsBoundary: !Ref TaskRolePermissionBoundaryPolicy               
        
 TaskRolePermissionBoundaryPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - "cloudwatch:PutMetricData"
              - "logs:PutLogEvents"
              - 's3:Get*'
              - 's3:List*'
              - 's3:PutObject'
              - 'sns:Get*'
              - 'sns:List*'
              - 'sns:Send*'
            Resource: '*'

We have indicated that in no case should this role be allowed to have permissions beyond those listed for CloudWatch, CloudWatch Logs, S3 and SNS. If the platform engineering team decides to enable another type of resource in the component (for example, a DynamoDB table), you can enable it by just modifying TaskRolePermissionBoundaryPolicy to incorporate it.

You can read more about IAM Permission Boundaries in the documentation here.

Conclusion

Components provide customers with additional flexibility in their templates: platform engineers can use components to allow developers to incorporate the resources they need on top of a centrally managed common architecture. Components allow platform engineers to support even more use cases with a limited number of templates, further enabling platform teams to scale.

We plan to continue supporting components with new features and types of components that support other styles of customization, such as templatized components or components that can consume service provisioning outputs, and can be used, for example, to create custom alarms beyond those included in the service template.

If there are other use cases that you’d like us to support, please open a new issue in our public roadmap.

If you want to read more about components, check out our documentation here.

Frank Retief headshot

Frank Retief

Frank is a Sr Software Engineer at AWS. He graduated from Cape Town with a Masters’ in Electrical and Computer Engineering. Frank is one of the AWS Proton team leads and especially enjoys operations work like metrics, alarms, and deployments.