AWS Cloud Operations Blog

How to automate Puppet manifest testing and delivery in AWS OpsWorks for Puppet Enterprise

Organizations that use AWS OpsWorks Puppet Enterprise can use AWS services that manage, test, and deploy code to create a continuous integration and continuous deployment (CI/CD) infrastructure.

In this blog post, we will show how you can use AWS CloudFormation,  Amazon S3, AWS CodeCommit, AWS CodeBuild, AWS Systems Manager, and AWS CodePipeline to set up a pipeline to automatically test your Puppet manifests and deploy them to your Puppet Enterprise master. If you are new to the concept of CI/CD, see the Practicing Continuous Integration and Continuous Delivery on AWS white paper.

The following example shows you how to create an AWS CloudFormation stack that contains this infrastructure, to create a pipeline that pushes code to a Puppet Enterprise master. By using CodeCommit, a Puppet developer can simply push the changes to the repository. The push triggers the pipeline as configured with AWS CloudFormation, and then sends the updated repository code to be tested with CodeBuild, which stops the pipeline in the event of a failure. Upon success, the pipeline continues after the user intervenes to manually approve the deployment and uploads the changes to the Puppet Enterprise master. The credentials for the Puppet Enterprise master are obtained through Systems Manager.

Prerequisites

Before you use the AWS CloudFormation example, follow these steps to create a CodeCommit repository and set up the OpsWorks Puppet server.

  1. Follow these steps in the AWS OpsWorks User Guide to create a CodeCommit repository and a local branch named production.
  2. Using the HTTPS URL of the CodeCommit repository, follow these steps in the AWS OpsWorks User Guide to create an OpsWorks for Puppet Enterprise master. Download the credentials and starter kit from the OpsWorks for Puppet Enterprise console.
  3. Unzip the Starter Kit in your Workstation and zip up the .config/ directory to push to your S3 Bucket using the following commands. This will contain the configurations that CodeBuild will use to access your Puppet Server:
zip -r credentials_starter_kit.zip .config/
aws s3 cp credentials_starter_kit.zip s3://yourS3Bucket

4. In your workstation (a separate instance or an on-premises workstation), unzip the starter kit. Make sure you are in the root of the starter kit directory and then run the following commands:

Copy the master SSH key to your workstation

aws --region=us-east-1 opsworks-cm describe-servers --server-name <your-server-name> --query "Servers[0].EngineAttributes[?Name=='PUPPET_API_CA_CERT'].Value" --output text >| .config/ssl/cert/ca.pem

Get the token to authenticate with the Puppet server. Make sure it has a long lifetime, as shown here.

puppet-access login --config-file .config/puppetlabs/client-tools/puppet-access.conf --lifetime 180d

Use this bash command to place the token as a parameter in Systems Manager. For more information, see put-parameter in the AWS CLI Reference.

tokenvalue=$(cat .config/puppetlabs/token) && aws ssm put-parameter --name "puppet-token" --type "String" --value "$tokenvalue" --overwrite

Add your code to the CodeCommit repository

Create, in the control-repo directory that comes with the starter-kit, a directory named aws/. This directory is used to define your build process defined by the following buildspec.yml files, which we will create:

.
|– buildspec-test.yml           # Buildspec intended for Manifest Testing
|– buildspec-upload.yml     # Buildspec intended for Manifest Delivery

buildspec-test.yml

In this file we define tests to run against your Puppet code from the starterkit control-repo-example. We will use the puppet parser and puppet-lint to check for validity of your code. CodeBuild uses the instructions of the file, executes them and will either pass or fail your build process. The following buildspec-test.yml was created based on the control-repo-example application created in your OpsWorks for Puppet Enterprise starter kit. You can edit it, as appropriate, for your own application.

version: 0.2

phases:
  build:
    commands:
     - ls -lash
     - apt-get update -y && apt-get install python-pip unzip -y
     - pip install awscli
     - gem install puppet-lint
     - echo "Validating Code With Puppet Parser"
     - puppet parser validate manifests/
     - puppet parser validate scripts/
     - puppet parser validate site/
     - puppet parser validate hieradata/
     - echo "Validating Code With Puppet-Lint"
     - puppet-lint manifests/
     - puppet-lint scripts/
     - puppet-lint site/
     - puppet-lint hieradata/
     - echo "Code Validated Successfully"

buildspec-upload.yml

When the tests are complete and manually approved in the pipeline, CodeBuild uploads the code to the Puppet server using the security token entered in a parameter in step 4 of Prerequisites. The following buildspec-upload.yml file sets up the environment for CodeBuild, like installing the AWS CLI and Puppet Client Tools and downloads the starterkit from S3. It then authenticates with the Puppet server and deploys the code pushed to AWS CodeBuild, through AWS CodePipeline:

version: 0.2

phases:
  build:
    commands:
     - ls -lash
     - apt-get update -y && apt-get install python-pip unzip -y
     - pip install awscli
     - wget https://pm.puppet.com/pe-client-tools/2018.1.0/18.1.0/repos/deb/xenial/PC1/pe-client-tools_18.1.0-1xenial_amd64.deb
     - dpkg -i pe-client-tools_18.1.0-1xenial_amd64.deb
     - ln -s /opt/puppetlabs/client-tools/bin/puppet-access /usr/sbin/puppet-access
     - ln -s /opt/puppetlabs/client-tools/bin/puppet-code /usr/sbin/puppet-code     
     - aws s3 cp $PATHTOPUPPETCREDS .
     - unzip *starter_kit.zip -d starter_kit
     - find starter_kit -type d -name '.config' -exec cp -r {} . \;
     - ls -lash
     - aws --region $REGION opsworks-cm describe-servers --server-name $PUPPETSERVERNAME --query "Servers[0].EngineAttributes[?Name=='PUPPET_API_CA_CERT'].Value" --output text > .config/ssl/cert/ca.pem
     - echo $ACCESSTOKEN > .config/puppetlabs/token 
     - puppet-code deploy --all --wait --config-file .config/puppet-code.conf --token-file .config/puppetlabs/token

Your control-repo-example directory should look like the following after the buildspec files have been added:

~/control-repo/
|– environment.conf
|– README.MD
|– .git
|– hieradata
|– LICENSE
|– manifests
|– Puppetfile
|– scripts
|– site
|– aws
` — buildspec-test.yml
` — buildspec-upload.yml

Now we are ready to launch the AWS CloudFormation stack with the following template elements.

AWS CloudFormation parameters

The following parameters in the AWS CloudFormation template are required. These parameters are passed to CodeBuild so that the service can test and deploy the code and authenticate with the Puppet Enterprise server.

"Parameters": {
    "PuppetServerName": {
      "Type": "String",
      "Description": "AWS OpsWorks for Puppet Enterprise Server Name, e.g. puppet-demo"
    },
    "CodeCommitRepository": {
      "Type": "String",
      "Description": "CodeCommit Control Repository name, e.g. puppet-demo-repo"
    },
    "PuppetAccessToken": {
      "Type": "AWS::SSM::Parameter::Value<String>",
      "Description": "SSM Parameter Name Containing Access Token, e.g. puppet-token"
    },
    "PathToPuppetCreds": {
      "Type": "String",
      "Description": "Enter the S3 path where the starter kit is located. ex: s3://opsworks-cm-demo-bucket/Puppet-demo_starter_kit.zip",
      "MinLength": "10"
    }
  }

AWS CloudFormation resources

The following resources are used to create a CodeBuild project, a CodePipeline resource, an S3 bucket, and the associated IAM permissions. The permissions to allow these services to work together are also created for you.

IAM permissions

The AWS::IAM::Role is used to allow CodePipeline and CodeBuild to access resources. In the case of CodeBuild, this means the source code that will be obtained and deployed to the server.

"CodePipelineRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "",
              "Effect": "Allow",
              "Principal": {
                "Service": "codepipeline.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": "CodePipelinePuppetDemo",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Action": [
                    "s3:GetObject",
                    "s3:GetObjectVersion",
                    "s3:GetBucketVersioning",
                    "opsworks-cm:Describe*",
                    "opsworks-cm:UpdateServer"
                  ],
                  "Resource": "*",
                  "Effect": "Allow"
                },
                {
                  "Action": [
                    "s3:PutObject"
                  ],
                  "Resource": [
                    "arn:aws:s3:::codepipeline*"
                  ],
                  "Effect": "Allow"
                },
                {
                  "Action": [
                    "ec2:*",
                    "cloudwatch:*",
                    "s3:*",
                    "sns:*",
                    "cloudformation:*",
                    "iam:PassRole"
                  ],
                  "Resource": "*",
                  "Effect": "Allow"
                },
                {
                  "Action": [
                    "codebuild:BatchGetBuilds",
                    "codebuild:StartBuild"
                  ],
                  "Resource": "*",
                  "Effect": "Allow"
                },
                {
                  "Action": [
                    "codecommit:CancelUploadArchive",
                    "codecommit:GetBranch",
                    "codecommit:GetCommit",
                    "codecommit:GetUploadArchiveStatus",
                    "codecommit:UploadArchive"
                  ],
                  "Resource": "*",
                  "Effect": "Allow"
                }
              ]
            }
          }
        ]
      }
    },
    "CodeBuildRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Action": [
                "sts:AssumeRole"
              ],
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "codebuild.amazonaws.com"
                ]
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "Path": "/",
        "Policies": [
          {
            "PolicyName": "CodeBuildAccess",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Action": [
                    "ec2:*",
                    "cloudwatch:*",
                    "logs:*",
                    "sns:*",
                    "s3:CreateBucket",
                    "s3:GetObject",
                    "s3:List*",
                    "s3:PutObject",
                    "opsworks-cm:*"
                  ],
                  "Effect": "Allow",
                  "Resource": "*"
                }
              ]
            }
          }
        ]
      }
    },

 

S3 bucket

To create an artifact store for CodePipeline, we can add an S3 bucket resource to our AWS CloudFormation template. This bucket is used by CodePipeline to store the revisions that go through the pipeline.

"PipelineArtifactStoreBucket": {
      "Type": "AWS::S3::Bucket"
    },

CodeBuild projects

To test and then deploy the code to the Puppet server, we will create two CodeBuild projects: CodeBuildProjectUpload and CodeBuildProjectTest. These projects will use the buildspec-upload.yml and buildspec-test.yml files we created earlier. It will take in the AWS CloudFormation parameters created as environment variables for the two projects to function correctly.

"CodeBuildProjectUpload": {
      "Type": "AWS::CodeBuild::Project",
      "Properties": {
        "Artifacts": {
          "Type": "CODEPIPELINE"
        },
        "Description": "AWS CodeBuild Project for Puppet manifest upload",
        "Environment": {
          "Type": "LINUX_CONTAINER",
          "ComputeType": "BUILD_GENERAL1_SMALL",
          "Image": "puppet/puppetserver",
          "EnvironmentVariables": [
            {
              "Name": "PATHTOPUPPETCREDS",
              "Value": {
                "Ref": "PathToPuppetCreds"
              }
            },
            {
              "Name": "PUPPETSERVERNAME",
              "Value": {
                "Ref": "PuppetServerName"
              }
            },
            {
              "Name": "REGION",
              "Value": {
                "Ref": "AWS::Region"
              }
            },
            {
              "Name": "ACCESSTOKEN",
              "Value": {
                "Ref": "PuppetAccessToken"
              }
            }
          ]
        },
        "Name": "Puppet-Manifest-Upload",
        "ServiceRole": {
          "Ref": "CodeBuildRole"
        },
        "Source": {
          "Type": "CODEPIPELINE",
          "BuildSpec": "aws/buildspec-upload.yml"
        },
        "TimeoutInMinutes": 10
      }
    },
    "CodeBuildProjectTest": {
      "Type": "AWS::CodeBuild::Project",
      "Properties": {
        "Artifacts": {
          "Type": "CODEPIPELINE"
        },
        "Description": "AWS CodeBuild Project for Puppet manifest testing",
        "Environment": {
          "Type": "LINUX_CONTAINER",
          "ComputeType": "BUILD_GENERAL1_SMALL",
          "Image": "puppet/puppetserver",
          "EnvironmentVariables": [
            {
              "Name": "PUPPETSERVERNAME",
              "Value": {
                "Ref": "PuppetServerName"
              }
            },
            {
              "Name": "REGION",
              "Value": {
                "Ref": "AWS::Region"
              }
            },
            {
              "Name": "ACCESSTOKEN",
              "Value": {
                "Ref": "PuppetAccessToken"
              }
            }
          ]
        },
        "Name": "Puppet-Manifest-Test",
        "ServiceRole": {
          "Ref": "CodeBuildRole"
        },
        "Source": {
          "Type": "CODEPIPELINE",
          "BuildSpec": "aws/buildspec-test.yml"
        },
        "TimeoutInMinutes": 10
      }
    },

Create the pipeline

To bring all of the elements together, we will create the CodePipeline resource. It will be configured to automatically push your code through the testing and deployment stages after a push is made to your CodeCommit repository. For security purposes, there is a manual stage that requires the user to approve the deployment before the code is deployed to the Puppet server.

You’ll see that it pulls the code from the production branch of the CodeCommit repository. This is because the Puppet server reserves the master branch for its own use. For more information, see Configure the Puppet Server Using the Starter Kit in the AWS OpsWorks User Guide.

"PuppetTestingPipeline": {
      "Type": "AWS::CodePipeline::Pipeline",
      "Properties": {
        "Name": "PuppetTestingPipeline",
        "ArtifactStore": {
          "Type": "S3",
          "Location": {
            "Ref": "PipelineArtifactStoreBucket"
          }
        },
        "RestartExecutionOnUpdate": "False",
        "RoleArn": {
          "Fn::GetAtt": [
            "CodePipelineRole",
            "Arn"
          ]
        },
        "Stages": [
          {
            "Name": "Source",
            "Actions": [
              {
                "InputArtifacts": [],
                "Name": "Source",
                "ActionTypeId": {
                  "Category": "Source",
                  "Owner": "AWS",
                  "Version": "1",
                  "Provider": "CodeCommit"
                },
                "OutputArtifacts": [
                  {
                    "Name": "MyApp"
                  }
                ],
                "Configuration": {
                  "RepositoryName": {
                    "Ref": "CodeCommitRepository"
                  },
                  "BranchName": "production",
                  "PollForSourceChanges": "True"
                },
                "RunOrder": 1
              }
            ]
          },
          {
            "Name": "CodeBuildTest",
            "Actions": [
              {
                "InputArtifacts": [
                  {
                    "Name": "MyApp"
                  }
                ],
                "Name": "CodeBuildTest",
                "ActionTypeId": {
                  "Category": "Test",
                  "Owner": "AWS",
                  "Version": "1",
                  "Provider": "CodeBuild"
                },
                "OutputArtifacts": [],
                "Configuration": {
                  "ProjectName": {
                    "Ref": "CodeBuildProjectTest"
                  }
                },
                "RunOrder": 2
              }
            ]
          },
          {
            "Name": "ApproveUpload",
            "Actions": [
              {
                "InputArtifacts": [],
                "Name": "ApproveUpload",
                "ActionTypeId": {
                  "Category": "Approval",
                  "Owner": "AWS",
                  "Version": "1",
                  "Provider": "Manual"
                },
                "OutputArtifacts": [],
                "RunOrder": 3
              }
            ]
          },
          {
            "Name": "CodeBuildUpload",
            "Actions": [
              {
                "InputArtifacts": [
                  {
                    "Name": "MyApp"
                  }
                ],
                "Name": "CodeBuildUpload",
                "ActionTypeId": {
                  "Category": "Test",
                  "Owner": "AWS",
                  "Version": "1",
                  "Provider": "CodeBuild"
                },
                "OutputArtifacts": [],
                "Configuration": {
                  "ProjectName": {
                    "Ref": "CodeBuildProjectUpload"
                  }
                },
                "RunOrder": 4
              }
            ]
          }
        ]
      }
    }
  }

Summary

This blog post demonstrated how organizations will benefit from continuous integration, continuous delivery (CI/CD) and AWS tooling to rapidly test and deploy Puppet manifest updates on AWS OpsWorks for Puppet Enterprise servers. To learn more about AWS OpsWorks for Puppet Enterprise, visit here.

About the Authors

 

Sai Charan Teja Gopaluni is a Cloud Support Engineer at Amazon Web Services. He is a subject matter expert for AWS OpsWorks and Amazon ECS. In his role, he enjoys assisting customers in using Configuration  Management and Container Management technologies. Outside of work, he enjoys playing Tennis and Soccer.

 

 

 

 

Clodagh Brady is a Cloud Support Engineer at Amazon Web Services. In her role, she enjoys supporting customers with their Opsworks and other DevOps configurations. Outside of work she enjoys yoga and general tom-foolery.