Create your first Attini project

Scenario

We recommend that you deploy your own project instead of the example templates below.

In this example scenario, I have 3 CloudFormation templates that I want to deploy using Attini:

├── dynamo-db.yaml
├── lambda.yaml
└── sns-topic.yaml

Application architecture

I have one SNS topic that triggers a Lambda function. The Lambda function then saves the event to a DynamoDB table.

MyFirstProject-Solution


Generate skeleton files

Open a shell in your project directory and run the command:

attini init-project skeleton

The command should generate 3 files attini-config.yaml, .attini-ignore and deployment-plan.yaml so your directory should look like this:

├── .attini-ignore
├── attini-config.yaml
├── deployment-plan.yaml
├── dynamo-db.yaml
├── lambda.yaml
└── sns-topic.yaml

Find more information about the .attini-ignore, click here


Configure attini-config.yaml

Find detailed information in the API reference.

Open attini-config.yaml and read through the configuration options and update it according to your requirements.

The most important thing is to configure the distributionName, see Naming a distribution for more information.

distributionName: skeleton
initDeployConfig:
  template: /deployment-plan.yaml
  stackName: ${environment}-${distributionName}-deployment-plan
  parameters:
    default:
      parameterKey: parameterValue
    environmentName:
      parameterKey: parameterValue


package: # When you use the Attini CLI to "package" a distribution, these instructions will be used, find more info here https://docs.attini.io/api-reference/attini-configuration.html
  prePackage:
    commands: # These shell commands will be executed on a temporary copy of your files so they will not affect your source files
      - echo "Setting the distribution id to random"
      - attini configure set-dist-id --random
        # Here we are using the Attini CLI to set the distribution id, if you use the "--id" flag instead of "--random" you can set this to any value you want
        # We recommend you use something linked to your source control, for example, a git commit.

Configure the Attini deployment plan

First step

Find detailed information in the API reference.

If you open the deployment-plan.yaml, you should see a template that looks like this:

AWSTemplateFormatVersion: "2010-09-09"

# https://docs.attini.io/getting-started/create-your-first-deployment-plan.html
Transform:
  - AttiniDeploymentPlan
  - AWS::Serverless-2016-10-31

Parameters:
  AttiniEnvironmentName: # This is automatically configured by the Attini Framework, find more info here https://docs.attini.io/api-reference/cloudformation-configuration.html#framework-parameters
    Type: String

  ParameterKey:
    Type: String

Resources:

  DeploymentPlan:
    Type: Attini::Deploy::DeploymentPlan # https://docs.attini.io/api-reference/deployment-plan.html
    Properties:
      DeploymentPlan:
        StartAt: Step1 # https://docs.aws.amazon.com/step-functions/latest/dg/concepts-amazon-states-language.html
        States:
          Step1:
            Type: AttiniCfn # https://docs.attini.io/api-reference/deployment-plan-types.html
            Properties:
              StackName: StackName # fill in your Cloudformation stack name
              Template: /Template.yaml
              # ExecutionRoleArn: Its recommended to use ExecutionRole or StackRole to create a "least privileged" deployment.
              # Find more information here https://docs.attini.io/architecture/securing-the-framework.html#executionrole
              Parameters:
                ParameterValue: !Ref ParameterValue
            End: True

Now we want to add our own templates and configure the deployment plan to deploy them for us.

Let’s start with the SNS topic template and add it to the deployment plan. We also create an IAM role that Attini will use for deploying the template.

For more information, see AttiniCfn type and CloudFormation configuration.

DeploymentPlan:
  Type: Attini::Deploy::DeploymentPlan
  Properties:
    DeploymentPlan:
      StartAt: Sns # Give your steps appropriate names
      States:
        Sns: # Give your steps appropriate names
          Type: AttiniCfn
          Properties:
            StackName: !Sub ${AttiniEnvironmentName}-sns-topic # Fill in your Cloudformation stack name. Its good practice to include the environment name
            Template: /sns-topic.yaml # Reference your templates location i the distribution.
            ExecutionRoleArn: !GetAtt DeploymentPlanExecutionRole.Arn # Reference the role you want Attini it use when we deploy stack
            Parameters:
              TopicName: !Sub ${AttiniEnvironmentName}-topic
          End: True


DeploymentPlanExecutionRole:
  Type: AWS::IAM::Role
  Properties:
    Description: Attini execution role used to create CloudFormation stacks
    AssumeRolePolicyDocument:
      Version: 2012-10-17
      Statement:
        -
          Effect: Allow
          Principal:
            AWS:
              !Sub arn:aws:iam::${AWS::AccountId}:role/attini/attini-action-role-${AWS::Region}
          Action: sts:AssumeRole
    ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonSNSFullAccess
      - arn:aws:iam::aws:policy/IAMFullAccess
      - arn:aws:iam::aws:policy/AWSLambda_FullAccess
      - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
      - !Sub arn:aws:iam::${AWS::AccountId}:policy/attini-cfn-policy-${AWS::Region} # This policy id created by attini-setup and is required on all CloudFormation stacks roles.
    Policies:
      - PolicyName: inline-policy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
                - cloudformation:Get*
                - cloudformation:List*
                - cloudformation:Describe*
                - cloudformation:CreateStack
                - cloudformation:DeleteStack
                - cloudformation:CreateChangeSet
                - cloudformation:UpdateStack
                - cloudformation:CreateTag
                - cloudformation:DeleteTag
              Resource: "*"
            - Effect: Allow
              Action:
                - s3:GetObject
              Resource:
                - !Sub arn:aws:s3:::attini-artifact-store-${AWS::Region}-${AWS::AccountId}/*

Now try to do run attini deploy run . to see if it works.


Parallel steps

Now, we want to deploy our DynamoDB table. There are no dependencies between the Sns and the Dynamo step so let’s do them in parallel:

DeploymentPlan:
  Type: Attini::Deploy::DeploymentPlan
  Properties:
    DeploymentPlan:
      StartAt: Step1
      States:
        Step1:
          Type: Parallel
          End: True
          Branches:
            - StartAt: Sns
              States:
                Sns:
                  Type: AttiniCfn
                  Properties:
                    StackName: !Sub ${AttiniEnvironmentName}-sns-topic
                    Template: /sns-topic.yaml
                    ExecutionRoleArn: !GetAtt DeploymentPlanExecutionRole.Arn
                    Parameters:
                      TopicName: !Sub ${AttiniEnvironmentName}-topic
                  End: True

            - StartAt: Dynamo
              States:
                Dynamo:
                  Type: AttiniCfn
                  Properties:
                    StackName: !Sub ${AttiniEnvironmentName}-dynamo-db
                    Template: /dynamo-db.yaml
                    ExecutionRoleArn: !GetAtt DeploymentPlanExecutionRole.Arn
                    Parameters:
                      TableName: !Sub ${AttiniEnvironmentName}-table
                  End: True

And let’s iterate one more time with the command attini deploy run .


Working with the payload

Now we want to deploy our Lambda function, and we want to use the Attini deployment plan payload to configure it (the payload is an excellent way to manage dependencies and other automatic configurations).

However, there is a problem when using the AWS Step Function parallel feature. Its output contains the outputs from the different branches, formatted as a list. This makes it hard to query the data. This is easily fixed using the AttiniMergeOutput type.

DeploymentPlan:
  Type: Attini::Deploy::DeploymentPlan
  Properties:
    DeploymentPlan:
      StartAt: Step1
      States:
        Step1:
          Type: Parallel
          Next: MergeOutputsFromStep1
          Branches:
            - StartAt: Sns
              States:
                Sns:
                  Type: AttiniCfn
                  Properties:
                    StackName: !Sub ${AttiniEnvironmentName}-sns-topic
                    Template: /sns-topic.yaml
                    ExecutionRoleArn: !GetAtt DeploymentPlanExecutionRole.Arn
                    Parameters:
                      TopicName: !Sub ${AttiniEnvironmentName}-topic
                  End: True

            - StartAt: Dynamo
              States:
                Dynamo:
                  Type: AttiniCfn
                  Properties:
                    StackName: !Sub ${AttiniEnvironmentName}-dynamo-db
                    Template: /dynamo-db.yaml
                    ExecutionRoleArn: !GetAtt DeploymentPlanExecutionRole.Arn
                    Parameters:
                      TableName: !Sub ${AttiniEnvironmentName}-table
                  End: True

        MergeOutputsFromStep1:
          Type: AttiniMergeOutput
          Next: Lambda

        Lambda:
          Type: AttiniCfn
          Properties:
            StackName: !Sub ${AttiniEnvironmentName}-lambda
            Template: /lambda.yaml
            ExecutionRoleArn: !GetAtt DeploymentPlanExecutionRole.Arn
            Parameters:
              TopicArn.$: $.output.Sns.TopicArn # see deployment plan payload
              TableName.$: $.output.Dynamo.TableName
          End: True

The AttiniMergeOutput step merged the outputs from the parallel branches so that we can easily query them. We can then read data from previous steps in the deployment plan from the “output” section of the payload.

Lets iterate one more time using attini deploy run ..


Creating an configurations file

If we want more advanced configuration options we need to create a configuration file.

The lambda.yaml template has a parameter called MemorySize which defaults to 128. Now we want to override this using a configuration file. Create a file called lambda-config.yaml in our project:

├── .attini-ignore
├── attini-config.yaml
├── deployment-plan.yaml
├── dynamo-db.yaml
├── lambda-config.yaml
├── lambda.yaml
└── sns-topic.yaml

And add this config to it:

parameters:
  MemorySize: 512

Now add a ConfigFile to our Lambda step, like this:

Lambda:
  Type: AttiniCfn
  Properties:
    StackName: !Sub ${AttiniEnvironmentName}-lambda
    Template: /lambda.yaml
    ExecutionRoleArn: !GetAtt DeploymentPlanExecutionRole.Arn
    Parameters:
      TopicArn.$: $.output.Sns.TopicArn
      TableName.$: $.output.Dynamo.TableName
    ConfigFile: /lambda-config.yaml
  End: True

There are a lot of configuration features available like:

  1. Configuration inheritance

  2. Fallback properties

  3. SSM Parameters

  4. To integrate the configuration file with the payload, see AttiniCfn Variables

  5. If you are planing to create multiple environments, see our recommended config pattern Multi-environment configuration pattern

The above information is hopefully enough to get you started with Attini but there are a lot more features to explore. For more information please see our Reference Architecture and API Reference.


General tips and tricks

Naming a distribution

Here are some tips on how to name your Attini distributions:

  • Your source control (Git) repository name is often a good Attini distribution name in simple setups.

  • Do not name an Attini distribution after a technology provider like attini or AWS. Instead, name it after its business area or function, for example, if you have an Attini distribution that manages an Amazon Redshift cluster, data-warehouse is a better name than amazon-redshift-attini-distribution.

  • Keep the name unique within your organization.


Setting the distributionId

It’s important to always give the Attini distribution a unique id. Otherwise, you will lose rollback and history support. The prePackage commands in the attini-config is a great place to configure this using the attini configure set-dist-id command.

Here are some guidelines on how to design your distributionId.

  • A Git commit (or parts of the Git commit) is a good id (or part of the id) because you will get a clear link between the Attini distribution and your source control.

  • Including a timestamp in the id can be good if you base the id on a Git commit id. Because sometimes you might want to re-package the same commit twice.

    This is especially important if your packaging scripts are dependent on external sources. For example, if you fetch the latest AMI version and inject it into your configuration files, the time of the packaging becomes important.

  • If you use semantic versioning for the distributionId, then make sure your prePackage commands automatically bump the patch version to ensure a unique id.

If you want to set the distributionId to an environment variable, your attini-config package section could look like this:

package:
  prePackage:
    commands:
      - attini configure set-dist-id --id $MY_GIT_COMMIT

A common scenario is that the variable $MY_GIT_COMMIT` is automatically configured on the build server, so now the attini package command won’t work on my local machine without additional configurations.

Attini works around this issue with the --environment-config-script flag. So now create a file in your distribution named local-config.sh and add this code to it:

export MY_GIT_COMMIT="my-local-build-`date '+%Y-%m-%d_%H:%M:%S'`"

Now you can use this command to package the distribution:

attini package -ec local-config.sh .

Templates

sns-topic.yaml

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  TopicName:
    Type: String

Resources:
  Topic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Ref TopicName

Outputs:
  TopicArn:
    Value: !Ref Topic

dynamo-db.yaml

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  TableName:
    Type: String

Resources:
  Table:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Ref TableName
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: id
          KeyType: HASH

lambda.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Parameters:
  TopicArn:
    Type: String

  TableName:
    Type: String

  MemorySize:
    Type: String
    Default: 128

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      Environment:
        Variables:
          DATABASE_NAME: !Ref TableName
      Events:
        SnsTrigger:
          Type: SNS
          Properties:
            Topic: !Ref TopicArn
      Handler: index.lambda_handler
      InlineCode: |
          import boto3
          import os
          import logging
          import json

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def lambda_handler(event, context):
            logger.info(f"Got event: {json.dumps(event)}")

            logger.info(f"My database table is {os.environ['DATABASE_NAME']}")
            # Add code to save data

            return event

      MemorySize: !Ref MemorySize
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref TableName
      Runtime: python3.9
      Timeout: 30