AWS DevOps & Developer Productivity Blog
Continuously building and delivering Maven artifacts to AWS CodeArtifact
Artifact repositories are often used to share software packages for use in builds and deployments. Java developers using Apache Maven use artifact repositories to share and reuse Maven packages. For example, one team might own a web service framework that is used by multiple other teams to build their own services. The framework team can publish the framework as a Maven package to an artifact repository, where new versions can be picked up by the service teams as they become available. This post explains how you can set up a continuous integration pipeline with AWS CodePipeline and AWS CodeBuild to deploy Maven artifacts to AWS CodeArtifact. CodeArtifact is a fully managed pay-as-you-go artifact repository service with support for software package managers and build tools like Maven, Gradle, npm, yarn, twine, and pip.
Solution overview
The pipeline we build is triggered each time a code change is pushed to the AWS CodeCommit repository. The code is compiled using the Java compiler, unit tested, and deployed to CodeArtifact. After the artifact is published, it can be consumed by developers working in applications that have a dependency on the artifact or by builds running in other pipelines. The following diagram illustrates this architecture.
All the components in this pipeline are fully managed and you don’t pay for idle capacity or have to manage any servers.
Prerequisites
This post assumes you have the following tools installed and configured:
- Java JDK 11
- Apache Maven 3.x
- AWS Command Line Interface (AWS CLI) – CLIv1 1.18.83 or later or CLIv2 2.0.54 or later
- Git client
Creating your resources
To create the CodeArtifact domain, CodeArtifact repository, CodeCommit, CodePipeline, CodeBuild, and associated resources, we use AWS CloudFormation. Save the provided CloudFormation template below as codeartifact-cicd-pipeline.yaml and create a stack:
---
Description: Code Artifact CI/CD Pipeline
Parameters:
GitRepoBranchName:
Type: String
Default: main
Resources:
ArtifactBucket:
Type: AWS::S3::Bucket
CodeArtifactDomain:
Type: AWS::CodeArtifact::Domain
Properties:
DomainName: !Sub "${AWS::StackName}-domain"
CodeArtifactRepository:
Type: AWS::CodeArtifact::Repository
Properties:
DomainName: !GetAtt CodeArtifactDomain.Name
RepositoryName: !Sub "${AWS::StackName}-repo"
CodeRepository:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryDescription: Maven artifact code repository
RepositoryName: !Sub "${AWS::StackName}-maven-artifact-repo"
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub "${AWS::StackName}-CodeBuild"
Artifacts:
Type: CODEPIPELINE
Environment:
EnvironmentVariables:
- Name: CODEARTIFACT_DOMAIN
Type: PLAINTEXT
Value: !GetAtt CodeArtifactDomain.Name
- Name: CODEARTIFACT_REPO
Type: PLAINTEXT
Value: !GetAtt CodeArtifactRepository.Name
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Source:
Type: CODEPIPELINE
BuildSpec: buildspec.yaml
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
RoleArn: !GetAtt CodePipelineServiceRole.Arn
Stages:
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: AWS
Version: '1'
Provider: CodeCommit
OutputArtifacts:
- Name: SourceBundle
Configuration:
BranchName: !Ref GitRepoBranchName
RepositoryName: !GetAtt CodeRepository.Name
RunOrder: '1'
- Name: Deliver
Actions:
- Name: CodeBuild
InputArtifacts:
- Name: SourceBundle
ActionTypeId:
Category: Build
Owner: AWS
Version: '1'
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuildProject
RunOrder: '1'
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: ''
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CodePipelinePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: CloudWatchLogsPolicy
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- "*"
- Sid: CodeCommitPolicy
Effect: Allow
Action:
- codecommit:GitPull
Resource:
- !GetAtt CodeRepository.Arn
- Sid: S3GetObjectPolicy
Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
Resource:
- !Sub "arn:aws:s3:::${ArtifactBucket}/*"
- Sid: S3PutObjectPolicy
Effect: Allow
Action:
- s3:PutObject
Resource:
- !Sub "arn:aws:s3:::${ArtifactBucket}/*"
- Sid: BearerTokenPolicy
Effect: Allow
Action:
- sts:GetServiceBearerToken
Resource: "*"
Condition:
StringEquals:
sts:AWSServiceName: codeartifact.amazonaws.com
- Sid: CodeArtifactPolicy
Effect: Allow
Action:
- codeartifact:GetAuthorizationToken
Resource:
- !Sub "arn:aws:codeartifact:${AWS::Region}:${AWS::AccountId}:domain/${CodeArtifactDomain.Name}"
- Sid: CodeArtifactPackage
Effect: Allow
Action:
- codeartifact:PublishPackageVersion
- codeartifact:PutPackageMetadata
- codeartifact:ReadFromRepository
Resource:
- !Sub "arn:aws:codeartifact:${AWS::Region}:${AWS::AccountId}:package/${CodeArtifactDomain.Name}/${CodeArtifactRepository.Name}/*"
- Sid: CodeArtifactRepository
Effect: Allow
Action:
- codeartifact:ReadFromRepository
- codeartifact:GetRepositoryEndpoint
Resource:
- !Sub "arn:aws:codeartifact:${AWS::Region}:${AWS::AccountId}:repository/${CodeArtifactDomain.Name}/${CodeArtifactRepository.Name}"
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: ''
Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CodePipelinePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketVersioning
Resource: !Sub "arn:aws:s3:::${ArtifactBucket}/*"
Effect: Allow
- Action:
- s3:PutObject
Resource:
- !Sub "arn:aws:s3:::${ArtifactBucket}/*"
Effect: Allow
- Action:
- codecommit:GetBranch
- codecommit:GetCommit
- codecommit:UploadArchive
- codecommit:GetUploadArchiveStatus
- codecommit:CancelUploadArchive
Resource:
- !GetAtt CodeRepository.Arn
Effect: Allow
- Action:
- codebuild:StartBuild
- codebuild:BatchGetBuilds
Resource:
- !GetAtt CodeBuildProject.Arn
Effect: Allow
- Action:
- iam:PassRole
Resource: "*"
Effect: Allow
Outputs:
CodePipelineArtifactBucket:
Value: !Ref ArtifactBucket
CodeRepositoryHttpCloneUrl:
Value: !GetAtt CodeRepository.CloneUrlHttp
CodeRepositorySshCloneUrl:
Value: !GetAtt CodeRepository.CloneUrlSsh
aws cloudformation deploy \
--stack-name codeartifact-pipeline \
--template-file codeartifact-cicd-pipeline.yaml \
--capabilities CAPABILITY_IAM
If you have a Maven project you want to use, you can use that. Otherwise, create a new one:
mvn archetype:generate \
-DgroupId=com.mycompany.app \
-DartifactId=my-app \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 -DinteractiveMode=false
Initialize a Git repository for the Maven project and add the CodeCommit repository that was created in the CloudFormation stack as a remote repository:
cd my-app
git init
CODECOMMIT_URL=$(aws cloudformation describe-stacks --stack-name codeartifact-pipeline --query "Stacks[0].Outputs[?OutputKey=='CodeRepositoryHttpCloneUrl'].OutputValue" --output text)
git remote add origin $CODECOMMIT_URL
Updating the POM file
The Maven project’s POM file needs to be updated with the distribution management section. This lets Maven know where to publish artifacts. Add the distributionManagement section inside the project element of the POM. Be sure to update the URL with the correct URL for the CodeArtifact repository you created earlier. You can find the CodeArtifact repository URL with the get-repository-endpoint CLI command:
aws codeartifact get-repository-endpoint --domain codeartifact-pipeline-domain --repository codeartifact-pipeline-repo --format maven
Add the following to the Maven project’s pom.xml:
<distributionManagement>
<repository>
<id>codeartifact</id>
<name>codeartifact</name>
<url>Replace with the URL from the get-repository-endpoint command</url>
</repository>
</distributionManagement>
Creating a settings.xml file
Maven needs credentials to use to authenticate with CodeArtifact when it performs the deployment. CodeArtifact uses temporary authorization tokens. To pass the token to Maven, a settings.xml file is created in the top level of the Maven project. During the deployment stage, Maven is instructed to use the settings.xml in the top level of the project instead of the settings.xml that normally resides in $HOME/.m2. Create a settings.xml in the top level of the Maven project with the following contents:
<settings>
<servers>
<server>
<id>codeartifact</id>
<username>aws</username>
<password>${env.CODEARTIFACT_TOKEN}</password>
</server>
</servers>
</settings>
Creating the buildspec.yaml file
CodeBuild uses a build specification file with commands and related settings that are used during the build, test, and delivery of the artifact. In the build specification file, we specify the CodeBuild runtime to use pre-build actions (update AWS CLI), and build actions (Maven build, test, and deploy). When Maven is invoked, it is provided the path to the settings.xml created in the previous step, instead of the default in $HOME/.m2/settings.xml. Create the buildspec.yaml as shown in the following code:
version: 0.2
phases:
install:
runtime-versions:
java: corretto11
pre_build:
commands:
- pip3 install awscli --upgrade --user
build:
commands:
- export CODEARTIFACT_TOKEN=`aws codeartifact get-authorization-token --domain ${CODEARTIFACT_DOMAIN} --query authorizationToken --output text`
- mvn -s settings.xml clean package deploy
Running the pipeline
The final step is to add the files in the Maven project to the Git repository and push the changes to CodeCommit. This triggers the pipeline to run. See the following code:
git checkout -b main
git add settings.xml buildspec.yaml pom.xml src
git commit -a -m "Initial commit"
git push --set-upstream origin main
Checking the pipeline
At this point, the pipeline starts to run. To check its progress, sign in to the AWS Management Console and choose the Region where you created the pipeline. On the CodePipeline console, open the pipeline that the CloudFormation stack created. The pipeline’s name is prefixed with the stack name. If you open the CodePipeline console before the pipeline is complete, you can watch each stage run (see the following screenshot).
If you see that the pipeline failed, you can choose the details in the action that failed for more information.
Checking for new artifacts published in CodeArtifact
When the pipeline is complete, you should be able to see the artifact in the CodeArtifact repository you created earlier. The artifact we published for this post is a Maven snapshot. CodeArtifact handles snapshots differently than release versions. For more information, see Use Maven snapshots. To find the artifact in CodeArtifact, complete the following steps:
- On the CodeArtifact console, choose Repositories.
- Choose the repository we created earlier named myrepo.
- Search for the package named my-app.
- Choose the my-app package from the search results.
- Choose the Dependencies tab to bring up a list of Maven dependencies that the Maven project depends on.
Cleaning up
To clean up the resources you created in this post, you need to remove them in the following order:
# Empty the CodePipeline S3 artifact bucket
CODEPIPELINE_BUCKET=$(aws cloudformation describe-stacks --stack-name codeartifact-pipeline --query "Stacks[0].Outputs[?OutputKey=='CodePipelineArtifactBucket'].OutputValue" --output text)
aws s3 rm s3://$CODEPIPELINE_BUCKET --recursive
# Delete the CloudFormation stack
aws cloudformation delete-stack --stack-name codeartifact-pipeline
Conclusion
This post covered how to build a continuous integration pipeline to deliver Maven artifacts to AWS CodeArtifact. You can modify this solution for your specific needs. For more information about CodeArtifact or the other services used, see the following: