MULTI-ENVIRONMENT DEPLOYMENTS USING AWS CODEDEPLOY AND MAVEN

This blog post details how to use AWS CodeDeploy and Maven to create a deployment process that easily supports multiple independent environments, such as staging and production.

AWS CODEDEPLOY SETUP

Create an AWS CodeDeploy application with a name of your choice. In this example, the name application is used. Under the newly created application, create one deployment group for each environment. The deployment group names should match the environment identifiers: stagingprod.

S3 SETUP

Create an S3 bucket called application-deployment . Enable versioning for this bucket.

IAM SETUP

Create a user called application-deployment . Attach the following inline policy to this user:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "s3:PutObject",
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::application-deployment/*" 
            ]
        }
    ]
}

Also attach the AWS managed policy AWSCodeDeployDeployerAccess to this user. The policies permit this user to upload files to the S3 bucket application-deployment and to create deployments in AWS CodeDeploy. If the managed policy is too permissive, you can view its JSON source and create a modified inline policy that better suits your needs.

AWS CLI CONFIGURATION

Create an access key for the IAM user created in the previous section. Store the access credentials in ~/.aws/credentials like such:

[application-deployment]
aws_access_key_id = 
aws_secret_access_key =

Also modify or create ~/.aws/config

[profile application-deployment]
region=

This configuration declares a named profile application-deployment. This allows selecting between different credentials using the --profile command line option.

MAVEN CONFIGURATION

Add the following profile configuration to your project’s POM file:

<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <activatedProperties>devlt&;/activatedProperties>
            <build.profile.id>dev</build.profile.id>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>staging</id>
        <properties>
            <activatedProperties>staging</activatedProperties>
            <build.profile.id>staging</build.profile.id>
        </properties>
    </profile>
    <profile>
        <id>prod</id>
        <properties>
            <activatedProperties>prod</activatedProperties>
            <build.profile.id>prod</build.profile.id>
        </properties>
    </profile>
</profiles>

This supports the environments devstaging and prod, with dev being the default.

How to use the different build profiles varies depending on your application. In this example we have a Spring Boot application that has a base configuration file src/main/resources/application.properties and one additional configuration file for each environment: src/main/resources/application-<environment>.properties.

In the <build> section of your POM file, add the following configuration:

<filters>
    <filter>src/main/resources/application-${build.profile.id}.properties</filter>
    <filter>src/main/resources/application.properties</filter>
</filters>
<resources>
    <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
    </resource>
</resources>

The file src/main/resources/application.properties contains the following line:

spring.profiles.active=@activatedProperties@

The above Maven profile and filtering configuration ensures that the placeholder activatedProperties will be replaced with the environment identifier, such as staging. Furthermore, spring.profiles.active instructs Spring Boot which environment specific configuration file is to be used.

DEPLOYMENT SCRIPT

In this section we assume that running the Maven package goal produces a deployable artifact target/application.tar.gz. Please make the appropriate adjustments that suit your application. The script is placed in the project’s root directory and executed like such: ./deploy.sh staging.

The script uses AWS CLIjq and Maven command line executable to compile, package, upload and deploy the application. The script accepts a single argument, which specifies the environment identifier where the application is to be deployed.

#!/bin/bash -e

command -v aws >/dev/null 2>&1 || { echo "AWS CLI is required, but not installed. Aborting." >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq (https://stedolan.github.io/jq/) is required, but not installed. Aborting." >&2; exit 1; }
command -v mvn >/dev/null 2>&1 || { echo "mvn is required, but not installed. Aborting." >&2; exit 1; }

ENVIRONMENT="$1" 

if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "prod" ]] ; then
    >&2 echo "Unknown environment: $ENVIRONMENT. Must be either staging or prod." 
    exit 1
fi

# build a dependent project
mvn -Dmaven.test.failure.ignore=false -f ../dependent-project/pom.xml clean install

# build the application
mvn -P "$ENVIRONMENT" -Dmaven.test.failure.ignore=false clean package

# upload app package to Amazon S3

echo "Uploading app package to S3" 

PUT_RESULT=$(aws s3api put-object --bucket application-deployment --key "${ENVIRONMENT}/application.tar.gz" --body target/application.tar.gz --profile application-deployment)
ETAG=$(echo -n "$PUT_RESULT" | jq --raw-output '.ETag')
VERSION_ID=$(echo -n "$PUT_RESULT" | jq --raw-output '.VersionId')

if [ "$VERSION_ID" = "" ]; then
    >&2 echo "put-object did not return VersionId" 
    exit 1
fi

# use AWS CodeDeploy to deploy the application
DEPLOYMENT_ID=$(aws deploy create-deployment --application-name application --deployment-group-name "${ENVIRONMENT}" --deployment-config-name CodeDeployDefault.OneAtATime --s3-location "bucket=application-deployment,bundleType=tgz,eTag=${ETAG},version=${VERSION_ID},key=${ENVIRONMENT}/application.tar.gz" --profile application-deployment | jq --raw-output '.deploymentId')

echo "Created a deployment with ID ${DEPLOYMENT_ID}" 

watch --interval 5 aws deploy get-deployment --deployment-id "${DEPLOYMENT_ID}" --profile application-deployment

The first few lines perform sanity checks for the required software and command line argument.

The subsequent lines run the Maven command line executable. In this example a dependent project is built before the main application. The command line switch -Pis used to supply the script’s command line argument as the Maven profile.

Once the build process completes successfully, the application is uploaded to the S3 bucket application-deployment under the key ${ENVIRONMENT}/application.tar.gz. Because S3 object versioning is enabled, previously uploaded application packages will not be destroyed or overwritten, even though the S3 object key is the same for each deployment of an environment. Even if this deployment fails, the application package of the most recent successful deployment is still available to CodeDeploy, as each deployment references an S3 object by key and version id.

Finally, a CodeDeploy deployment is created in the environment’s deployment group and with the uploaded application package.

The last line uses watch to periodically output the status of the newly created deployment. If the script is used in a continuous integration environment, this line should be omitted as it will not terminate on its own.

Tilaa blogimme