In this guide we will demonstrate how to work with Attini deployments.
An Attini deployment is executed when you upload a distribution into an environment. After the upload, Attini will run the deployment on your behalf using serverless resources within your AWS account.
In this guide, we will first learn how to create an Attini environment and how to upload the distribution to it.
Then we will look at the deployment plan (pipeline) to learn how to reconfigure it according to our needs.
Before you can deploy a distribution to your AWS account you need to create an environment. This is done by running the
attini environment create
command. For example:
attini environment create stage
The above example will create an environment in your account called “stage”.
Attini has two types of environments, “test” and “production”. The only difference between
the two is that you will be prompted for an extra confirmation when deploying to a production environment.
Production is the default type, but you can create a test environment by using --type
option when creating the
environment.
We also need a distribution to deploy, and we can use the Attini CLI to create one for us. Let’s first create a folder for our distribution:
mkdir attini-hello-world
cd attini-hello-world
Then run the following command to create a basic hello world project:
attini init-project hello-world
Our project should now have the following file structure:
├── attini-config.yaml
├── deployment-plan.yaml
└── lambda-endpoint
└── lambda.yaml
The project contains an attini-config file, a deployment plan, and a folder with a CloudFormation template for a lambda. This example uses CloudFormation for creating the Lambda function but Attini has other types for different tasks. See the additional guides for more information.
We can now deploy the distribution by running the command:
attini deploy run .
The first argument for the attini deploy run
command is the path to the distribution to deploy.
If the path point towards a directory, the CLI will perform the attini package command
automatically.
This means that all options available for the attini package command
is also available for the attini deploy run
command.
However, they will only take effect if the distribution is not already packaged.
Let’s run the command 🚂 !
Success! As you noticed we were prompted to confirm the deployment because our environment is of the “production” type.
If we dont want to be prompted when deploying we can add the --force
option to the command.
Because we only have one environment in the account we did not have to specify it. If we had more then one environment
in the account the CLI would have asked us to choose one, We can also specify it with the --environment
option (-e
for short).
For example:
attini deploy run . -e stage --force
We have now successfully deployed a distribution. The following sections will take a closer look at the deployment plan and the Attini configuration file.
The deployment plan is a pipeline that will be executed within your AWS Account by a combination of serverless resources.
It’s defined in a CloudFormation template, which makes it easy to combine with any cloud resources you need. This also allows you to define it using CloudFormation abstractions like the CDK.
When you run a deployment, Attini will first create/update a dedicated deployment plan for your distribution in your environment. The deployment plan is defined in code and packaged with your distribution, making it easy to maintain at scale. This makes it possible to reuse your distribution for multiple environments while keeping the environments completely separated.
The deployment plan uses AWS StepFunction to execute the different steps in the deployment plan. It supports all StepFunction features, as well as a lot of Attini-specific ones. The deployment plan can be written using normal AWS State Language, but it also supports the Attini simplified syntax that eliminates a lot of clutter and makes it much easier to manage. Read more about the deployment plan in the documentation.
The deployment-plan.yaml file in our example project looks like this:
AWSTemplateFormatVersion: 2010-09-09
Transform:
- AttiniDeploymentPlan
- AWS::Serverless-2016-10-31
Parameters:
AttiniEnvironmentName:
Type: String
Resources:
HelloWorldDeploymentPlan:
Type: Attini::Deploy::DeploymentPlan
Properties:
DeploymentPlan:
- Name: Deploy_HelloWorldLambda
Type: AttiniCfn
Properties:
StackName: !Sub ${AttiniEnvironmentName}-hello-world-lambda
Template: /lambda-endpoint/lambda.yaml
- Name: Invoke_HelloWorldLambda
Type: AttiniLambdaInvoke
Parameters:
FunctionName.$: $.output.Deploy_HelloWorldLambda.FunctionName
If you are familiar with CloudFormation you might see alot of thing you recognize. The deployment-plan.yaml is a CloudFormation template, meaning we can use any CloudFormation features we need. But the “Attini::Deploy::DeploymentPlan” is a special Attini resource that needs the AttiniFramework to function.
As you can see the deployment plan contains a list of two steps with two different types. The “AttiniCfn” type will deploy a CloudFormation stack containing a lambda, and the “AttiniLambdaInvoke” will invoke the lambda after it is deployed. All steps, no matter the type, must have a unique name.
You can read more about the types used in these guides:
Or in the documentation.
Other useful guides about how to use work with deployment plans are:
Every distribution has to contain an Attini configuration file Attini configuration file. The Attini configuration file has to be located in the root of the project and can be either in JSON or YAML format. The file has to have one of the following names:
The configuration file in our example project looks like this:
distributionName: hello-world
initDeployConfig:
template: deployment-plan.yaml
stackName: ${environment}-${distributionName}-deployment-plan
package:
prePackage:
commands:
- attini configure set-dist-id --random
Because the deployment plan is created using CloudFormation, we need to provide a “template” and “stackName”. The stack name has to be unique within our AWS Account and region, so in the example above example we use the distribution name and the current environment name to create an appropriate name for the stack.
The package section is more extensively covered in the package a distribution guide.
When deploying to different environments you often need different configuration depending on what environment you are deploying to. Configuration for the deployment plan stack can be written in Attini configuration file and can be specified per environment. A simple configuration could look like this:
distributionName: attini-deployment-demo
initDeployConfig:
template: deployment-plan.yaml
stackName: ${environment}-${distributionName}-deployment-plan
parameters:
default:
Message: "We are testing"
production:
Message: "It's showtime!"
In the above example we specify a parameter called “Message” for the deployment plan stack. If the environment is called “production” the parameter value will be “It’s showtime!”, otherwise it will be “We are testing”. You can have more environment names depending on your needs. If the environment you are deploying to doesn’t match any of the names the “default” value will be used. The deployment plan template for the configuration above could look like this:
AWSTemplateFormatVersion: 2010-09-09
Transform:
- AttiniDeploymentPlan
- AWS::Serverless-2016-10-31
Parameters:
Message:
Type: String
RunnerDemo:
Type: Attini::Deploy::DeploymentPlan
Properties:
DeploymentPlan:
- Name: EchoWorld
Type: AttiniRunnerJob
Properties:
Commands:
- !Sub echo ${Message}
Because the “Message” is passed as a Parameter we can use normal CloudFormation to read it. You can also find more information about the AttiniRunnerJob here.
AWSTemplateFormatVersion: 2010-09-09
Transform: # These macros is needed for the Attini::Deploy::DeploymentPlan to work. You can add additional macros if you need it.
- 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
Resources:
HelloWorldDeploymentPlan:
Type: Attini::Deploy::DeploymentPlan # https://docs.attini.io/api-reference/deployment-plan/
Properties:
DeploymentPlan:
- Name: Deploy_HelloWorldLambda
Type: AttiniCfn
Properties:
StackName: !Sub ${AttiniEnvironmentName}-hello-world-lambda
Template: /lambda-endpoint/lambda.yaml
- Name: Invoke_HelloWorldLambda
Type: AttiniLambdaInvoke
Parameters:
FunctionName.$: $.output.Deploy_HelloWorldLambda.FunctionName
distributionName: hello-world
initDeployConfig:
template: deployment-plan.yaml # The path to the deployment plan template, find more info here: https://docs.attini.io/getting-started/create-your-first-deployment-plan.html
stackName: ${environment}-${distributionName}-deployment-plan # The stack name for the init deployment, this stack will for example be called "dev-hello-world-deployment-plan"
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/
prePackage:
commands: # These shell commands will be executed on a temporary copy of your files, so they will not affect your source files
- attini configure set-dist-id --random
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Parameters:
AttiniEnvironmentName:
Type: String
Resources:
HelloWorldLambda:
Type: AWS::Serverless::Function
Properties:
Description: !Sub Lambda that returns hello ${AttiniEnvironmentName} world
FunctionName: !Sub ${AttiniEnvironmentName}-hello-world-get-parameter
InlineCode: |
import os
def lambda_handler(event, context):
return f"Hello {os.environ['Env']} world"
Environment:
Variables:
Env: !Ref AttiniEnvironmentName
Handler: index.lambda_handler
Runtime: python3.9
FunctionUrlConfig:
AuthType: NONE
HelloWorldLambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${HelloWorldLambda}
RetentionInDays: 30
Outputs:
FunctionName:
Value: !Ref HelloWorldLambda
Url:
Value: !GetAtt HelloWorldLambdaUrl.FunctionUrl