Module Development
To create a new module, there are certain requirements needed to allow the CLI (and AWS CodeSeedeer) to deploy your code.
Required Files
Every module must have the following:
Optional Files
Create a new module
The CLI provides an init
method to create your skeleton module code. We will create a new module named mymodule
in the group mygroup
> seedfarmer init module -g mygroup -m mymodule
> cd modules/mygroup/mymodule
The strucuture for your module is in place. Edit the deploysepc.yaml
as needed. We provide a modulestack.template
file that can be edited for additional permissions, and that file needs to be renamed to modulestack.yaml
in order to be used.
For a deep-dive on the module creation command, see HERE.
Deployspec
Each Module must contain a deployspec.yaml
file. This file defines deployment instructions read by seedfarmer. These instructions include the external module metadata required, libraries/utilities to be installed, and deployment commands. The deployspec.yaml is very similar to the AWS CodeBuild buildspec.yaml implementing the phases structure and adding a module_dependencies section for declaring other modules whose metadata should be made to the module on deployment.
Structure
Below is a sample manifest that just ‘echo’ data to the environment runtime:
deploy:
phases:
install:
commands:
- pip install -r requirements.txt
pre_build:
commands:
- echo "Prebuild stage"
build:
commands:
- echo "bash deploy.sh"
post_build:
commands:
- echo "Deploy successful"
destroy:
phases:
install:
commands:
- pip install -r requirements.txt
pre_build:
commands:
- echo "Prebuild stage"
- echo "testing change"
build:
commands:
- echo "DESTROY!"
post_build:
commands:
- echo "Destroy successful"
build_type: BUILD_GENERAL1_LARGE
publishGenericEnvVariables: true
The deployspec is broken into 2 major areas of focus: deploy
and destroy
. Each of these areas have 4 distinct phases in which commands can be executed (ex. installing supporting libraries, setting environment variables, etc.) It is in these sections that AWS CodeSeeder makes calls to deploy/destroy on the modules’ behalf. The example below will highlight.
Deployspec Parameters of Interest
There are two parameters at the root level of importance:
build_type
publishGenericEnvVariables
The parameter build_type
allows module developers to choose the size of the compute instance AWS CodeSeeder will leverage as defined HERE. This parameter is defaulted to BUILD_GENERAL1_SMALL
The currently supported values are:
- BUILD_GENERAL1_SMALL
- BUILD_GENERAL1_MEDIUM
- BUILD_GENERAL1_LARGE
- BUILD_GENERAL1_2XLARGE
TThe parameter publishGenericEnvVariables
is a boolean and was implemented to support generic modules (deploy regardless of project name) and project-specif modules (ex ADDF). This parameter defaults to false
implying the prefix of the project to the pertient environment parameters in the codeubuild environment. When developing generic modules (modules for reuse regardless of project) this parameter MUST be set to true
.
This Pull Request goes into detail …please read. Here is an exerpt:
When creating a module, builders can now specify the optional publishGenericEnvVariables attribute in the module deployspec.yaml. When set to true the Env Variables passed to CodeBuild for SeedFarmer metadata (ProjectName, DeploymentName, ModuleName, etc) are prefixed with SEEDFARMER_ rather than the UPPER ProjectName. From the included exampleproj project in examples: EXAMPLEPROJ_DEPLOYMENT_NAME would be SEEDFARMER_DEPLOYMENT_NAME. And for Module Parameters, EXAMPLEPROJ_PARAMETER_SOME_PARAMETER would be SEEDFARMER_PARAMETER_SOME_PARAMETER.
Example
The following is an example deployspec that issues a series of commands. This is a project-specific module with a project named MYAPP
. This is only an example…
deploy:
phases:
install:
commands:
- npm install -g aws-cdk@2.20.0
- apt-get install jq
- pip install -r requirements.txt
build:
commands:
- aws iam create-service-linked-role --aws-service-name elasticmapreduce.amazonaws.com || true
- export ECR_REPO_NAME=$(echo $MYAPP_PARAMETER_FARGATE | jq -r '."ecr-repository-name"')
- aws ecr describe-repositories --repository-names ${ECR_REPO_NAME} || aws ecr create-repository --repository-name ${ECR_REPO_NAME}
- export IMAGE_NAME=$(echo $MYAPP_PARAMETER_FARGATE | jq -r '."image-name"')
- export COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- export IMAGE_TAG=${COMMIT_HASH:=latest}
- export REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_REPO_NAME
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- >
echo "MYAPP_PARAMETER_SHARED_BUCKET_NAME: ${MYAPP_PARAMETER_SHARED_BUCKET_NAME}"
- echo Building the Docker image...
- cd service/ && docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
- docker push $REPOSITORY_URI:latest && docker push $REPOSITORY_URI:$IMAGE_TAG
- cd.. && cdk deploy --all --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json
- export MYAPP_MODULE_METADATA=$(python -c "import json; file=open('cdk-exports.json'); print(json.load(file)['myapp-${MYAPP_DEPLOYMENT_NAME}-${MYAPP_MODULE_NAME}']['metadata'])")
destroy:
phases:
install:
commands:
- npm install -g aws-cdk@2.20.0
- pip install -r requirements.txt
build:
commands:
- cdk destroy --all --force --app "python app.py"
build_type: BUILD_GENERAL1_LARGE
The following is an example deployspec that issues a series of commands. This is a generic module. This is only an example…
deploy:
phases:
install:
commands:
- npm install -g aws-cdk@2.20.0
- apt-get install jq
- pip install -r requirements.txt
build:
commands:
- aws iam create-service-linked-role --aws-service-name elasticmapreduce.amazonaws.com || true
- export ECR_REPO_NAME=$(echo $SEEDFARMER_PARAMETER_FARGATE | jq -r '."ecr-repository-name"')
- aws ecr describe-repositories --repository-names ${ECR_REPO_NAME} || aws ecr create-repository --repository-name ${ECR_REPO_NAME}
- export IMAGE_NAME=$(echo $SEEDFARMER_PARAMETER_FARGATE | jq -r '."image-name"')
- export COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- export IMAGE_TAG=${COMMIT_HASH:=latest}
- export REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_REPO_NAME
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- >
echo "SEEDFARMER_PARAMETER_SHARED_BUCKET_NAME: ${SEEDFARMER_PARAMETER_SHARED_BUCKET_NAME}"
- echo Building the Docker image...
- cd service/ && docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
- docker push $REPOSITORY_URI:latest && docker push $REPOSITORY_URI:$IMAGE_TAG
- cd.. && cdk deploy --all --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json
- export SEEDFARMER_MODULE_METADATA=$(python -c "import json; file=open('cdk-exports.json'); print(json.load(file)['seedfarmer-${SEEDFARMER_DEPLOYMENT_NAME}-${SEEDFARMER_MODULE_NAME}']['metadata'])")
destroy:
phases:
install:
commands:
- npm install -g aws-cdk@2.20.0
- pip install -r requirements.txt
build:
commands:
- cdk destroy --all --force --app "python app.py"
build_type: BUILD_GENERAL1_LARGE
publishGenericEnvVariables: true
In the above examples, a different CDKv2 version is being installed as an example, AWS CLI commands are issued, and the actual deployment script (AWS CDK) is executed with the output of the CDK being written to SSM as a JSON document so other modules can leverage it.
Deployspec CLI Helper Commands
Special commands have been added to SeedFarmer CLI to support management and manipulation of metadata that modules can produce. You can see the commands available by running
seedfarmer metadata --help
Usage: seedfarmer metadata [OPTIONS] COMMAND [ARGS]...
Manage the metadata in a module deployment execution
Options:
--help Show this message and exit.
Commands:
add Add Output K,V to the Metadata
convert Convert the CDK Output of the module to SeedFarmer Metadata
depmod Get the Full Name of the Module
paramvalue Get the parameter value based on the suffix
NOTE: these commands can ONLY be run from the deployspec.yaml
.
SeedFarmer records module metadata from each module deployment so they can be passed to dependent modules as inputs. In the deployspec.yaml
, you must write the data to a file SEEDFARMER_MODULE_METADATA (or to <PROJECT>_MODULE_METADATA if not a generic module).
The commands automatically resolve the proper output file locations.
The commands are self-explanatory, but we will go over the highlights.
add - this command gives you the ability to add any K,V pair (or stringified JSON) to the outputs
depmod - short for Deployment Module, this command gives the fully resolved Deployment name of the module (<Project>-<Deployment>-<Group>-<Module> name format)
paramvalue - this will give the resolved env parameter value of the key. SeedFarmer lists all module-specific env parameters of the form <Project>-<Suffix>. If the module is generic, this becomes SEEDFARMER-<Suffix>. For example the SEEDFARMER_DEPLOYMENT_NAME is a valid env parameter, where the suffix is DEPLOYMENT_NAME
convert - this command is cdk-centric as most modules are using CDKv2, which outputs data to a file of your choosing. This command will convert that output to a JSON-stringified output that is then recorded in SSM. It supports
jq for parsing
referencing other json files in the bundle (the value referenced must be stringified JSON…else use the
add
command)
In the following deployspec.yaml
snippet, we demonstrate some uses of this command:
deploy:
phases:
build:
commands:
# execute the CDK
- cdk deploy --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json
- seedfarmer metadata add -k TestKeyValue -v TestKeyValueValue || true
- seedfarmer metadata add -j '{"JsonTest":"ValHere"}' || true
- export DEPMOD=$(seedfarmer metadata depmod)
- echo ${DEPMOD}
- seedfarmer metadata convert
- seedfarmer metadata convert -f cdk-exports.json # Does the SAME thing as the line above
- seedfarmer metadata convert -jq .${DEPMOD}.metadata # Does the SAME thing as the line above
- seedfarmer metadata convert -f cdk-exports-test.json -jq .${DEPMOD}.metadata # Looks for a file named cdk-exports-test.json
- echo $(seedfarmer metadata paramvalue -s DEPLOYMENT_NAME)
Module ReadMe
As part of the process to promote reusability and sharabiltiy of the modules, each module is required to have a README.md that talks directly to end users and describes:
the description of the module
the inputs - parameter names
required
optional
the outputs - the parameter names in JSON format
having a sample output is highly recommneded so other users cancan quickly reference in their modules
Example
Below is a sample of the sections in a README.md for the modules:
# OpenSearch Module
## Description
This module creates an OpenSearch cluster
## Inputs/Outputs
### Input Paramenters
#### Required
- `vpc-id`: The VPC-ID that the cluster will be created in
#### Optional
- `opensearch_data_nodes`: The number of data nodes, defaults to `1`
- `opensearch_data_nodes_instance_type`: The data node type, defaults to `r6g.large.search`
- `opensearch_master_nodes`: The number of master nodes, defaults to `0`
- `opensearch_master_nodes_instance_type`: The master node type, defaults to `r6g.large.search`
- `opensearch_ebs_volume_size`: The EBS volume size (in GB), defaults to `10`
### Module Metadata Outputs
- `OpenSearchDomainEndpoint`: the endpoint name of the OpenSearch Domain
`OpenSearchDomainName`: the name of the OpenSearch Domain
- `OpenSeearchDashboardUrl`: URL of the OpenSearch cluster dashboard
- `OpenSearchSecurityGroupId`: name of the DDB table created for Rosbag Scene Data
#### Output Example
```json
{
"OpenSearchDashboardUrl": "https://vpc-myapp-test-core-opensearch-aaa.us-east-1.es.amazonaws.com/_dashboards/",
"OpenSearchDomainName": "vpc-myapp-test-core-opensearch-aaa",
"OpenSearchDomainEndpoint": "vpc-myapp-test-core-opensearch-aaa.us-east-1.es.amazonaws.com",
"OpenSearchSecurityGroupId": "sg-0475c9e7efba05c0d"
}
ModuleStack
The modulestack (modulestack.yaml
) is an optional AWS Cloudformation file that contains the granular permissions that AWS Codeseeder will need to deploy your module. It is recommended to use a least-privelege policy to promote security best practices.
By default, the CLI uses AWS CDKv2, which assumes a role that has the permissions to deploy via CloudFormation and is the recommended practice. You have the ability to use the modulestack.yaml
to give additial permissions to AWS CodeSeeder
on your behalf.
Typical cases when you would use a modulestack.yaml
:
any time you are invoking AWS CLI in the deployspec (not in the scope of the CDK) - for example: copying files to S3
you prefer to use the AWSCLI v1 - in which a least-privilege policy is necessary for ALL AWS Services.
Initial Template
Below is a sample template that is provoded by the CLI. The Parameters
section is populated with the input provided from the CLI when deploying.
*** It DOES have a policy definiton that is wide open - you SHOULD CHANGE THIS - it is only a template!
AWSTemplateFormatVersion: 2010-09-09
Description: This template deploys a Module specific IAM permissions
Parameters:
DeploymentName:
Type: String
Description: The name of the deployment
ModuleName:
Type: String
Description: The name of the Module
RoleName:
Type: String
Description: The name of the IAM Role
Resources:
Policy:
Type: 'AWS::IAM::Policy'
Properties:
PolicyDocument:
Statement:
- Effect: Allow
Action: '*'
Resource: '*'
Version: 2012-10-17
PolicyName: "myapp-modulespecific-policy"
Roles: [!Ref RoleName]
Parameters
As mentioned above, we strongly recommend a least-priviledge policy to promote security best practices. The modulestack.yaml
automatically has access to the parameters that were defined in your manifest file. Those passed parameters can help make your policy more explicit by using parameter names to limit permissions to a resource.
Below is an example of how to make use of this functionality.
Let’s say we want to deploy the Cloud9 module. This module, on top of deploying a Cloud9 instance, also executes a few boto3 calls after the CDK deploys the Cloud9 environment. This example will focus on the boto3 call that modifies the volume of the instance. So we need to give permission to execute that boto3 call and we also want to restrict which instance to modify
Suppose this is our manifest. We want our modulestack.yaml to limit modification to our instance named cloud9-ml-project-dev
name: workbench
path: modules/workbench/cloud9/
parameters:
...
- name: instance_type
value: t3.micro
- name: instance_name
value: cloud9-ml-project-dev
...
The parameter names in your module manifest are resolved in CamelCase
in the modulestack.yaml file. The instance_name
parameter will resolve to InstanceName
. Back in our modulestack.yaml, under Parameters
, the manifest parameter instance_name
is added as InstanceName
. Now we can add a policy that will allow modification of volume of our specific instance by referencing the parameter we specified
...
Parameters:
InstanceName:
Type: String
Description: The name of the Cloud9 instance
Resources:
Policy:
Type: "AWS::IAM::Policy"
Properties:
PolicyDocument:
Statement:
- Effect: Allow
Action:
- "ec2:ModifyVolume"
Resource: "*"
Condition:
StringLike:
ec2:ResourceTag/Name: !Sub 'aws-cloud9-${InstanceName}-*'
...
Side note, the aws-cloud9-
prefix is added by the Cloud9 deployment automatically
Add the Manifests
Create a new module manifest (see manifests) and place it in the manifests/
directory, under a logical directory. If the deployment.yaml
manifest does not exist, create it also. Add your module manifest
to the deployment manifest
.