├── README.md ├── app └── index.html ├── appspec.yml ├── deploy ├── application_start.sh ├── before_install.sh ├── stack.yml └── validate_service.sh ├── lambda └── index.js └── setup.sh /README.md: -------------------------------------------------------------------------------- 1 | # Example of a Continuous Deliver pipeline with CodePipeline and CodeDeploy 2 | 3 | Creating a simple Continuous Delivery pipeline allowing you to push changes from your GitHub repository to your EC2 Instances. 4 | 5 | ## Services 6 | 7 | The following AWS services are used to create a Continuous Delivery pipeline. 8 | 9 | * CodePipeline 10 | * CodeDeploy 11 | * CloudFormation 12 | * EC2 13 | * S3 14 | * IAM 15 | 16 | ## Setup 17 | 18 | Fork this repository. 19 | 20 | Use the `./setup.sh` script to create a Continuous Delivery pipeline. 21 | 22 | Note: The script will create a CloudFormation stack which launches an EC2 instance into the default VPC of your default region. 23 | 24 | The script will ask you for: 25 | * GitHub repository 26 | * GitHub owner (username of individual or organisation) 27 | * GitHub [OAuth Token with access to Repo](https://github.com/settings/tokens). 28 | 29 | It might take a few minutes until the CodePipeline job has finished and the EC2 Instance is able to answer your HTTP request. -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 |

Automation for the people!

-------------------------------------------------------------------------------- /appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: app/index.html 5 | destination: /var/www/html 6 | hooks: 7 | BeforeInstall: 8 | - location: deploy/before_install.sh 9 | timeout: 180 10 | ApplicationStart: 11 | - location: deploy/application_start.sh 12 | timeout: 180 13 | ValidateService: 14 | - location: deploy/validate_service.sh 15 | timeout: 180 -------------------------------------------------------------------------------- /deploy/application_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | service httpd start -------------------------------------------------------------------------------- /deploy/before_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | yum install -y httpd -------------------------------------------------------------------------------- /deploy/stack.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: 'Continuous Delivery pipeline for static website' 4 | Parameters: 5 | GitHubOwner: 6 | Description: 'The owner of the GitHub repository.' 7 | Type: String 8 | GitHubOAuthToken: 9 | Description: 'The OAuthToken of the GitHub user.' 10 | Type: String 11 | GitHubRepo: 12 | Description: 'The GitHub repository.' 13 | Type: String 14 | VPC: 15 | Description: 'The VPC Id to launch the EC2 instance in.' 16 | Type: AWS::EC2::VPC::Id 17 | Subnet: 18 | Description: 'The Subnet Id to launch the EC2 instance in.' 19 | Type: AWS::EC2::Subnet::Id 20 | Mappings: 21 | RegionMap: 22 | 'ap-south-1': 23 | AMI: 'ami-cacbbea5' 24 | 'eu-west-1': 25 | AMI: 'ami-d41d58a7' 26 | 'ap-northeast-2': 27 | AMI: 'ami-a04297ce' 28 | 'ap-northeast-1': 29 | AMI: 'ami-1a15c77b' 30 | 'sa-east-1': 31 | AMI: 'ami-b777e4db' 32 | 'ap-southeast-1': 33 | AMI: 'ami-7243e611' 34 | 'ap-southeast-2': 35 | AMI: 'ami-55d4e436' 36 | 'eu-central-1': 37 | AMI: 'ami-0044b96f' 38 | 'us-east-1': 39 | AMI: 'ami-c481fad3' 40 | 'us-east-2': 41 | AMI: 'ami-71ca9114' 42 | 'us-west-1': 43 | AMI: 'ami-de347abe' 44 | 'us-west-2': 45 | AMI: 'ami-b04e92d0' 46 | Resources: 47 | ArtifactStore: 48 | Type: "AWS::S3::Bucket" 49 | Properties: 50 | VersioningConfiguration: 51 | Status: Enabled 52 | CodePipelineIAMRole: 53 | Type: 'AWS::IAM::Role' 54 | Properties: 55 | AssumeRolePolicyDocument: 56 | Version: '2012-10-17' 57 | Statement: 58 | - Effect: Allow 59 | Principal: 60 | Service: 61 | - 'codepipeline.amazonaws.com' 62 | Action: 63 | - 'sts:AssumeRole' 64 | Path: '/' 65 | Policies: 66 | - PolicyName: logs 67 | PolicyDocument: 68 | Version: '2012-10-17' 69 | Statement: 70 | - Action: 71 | - s3:GetObject 72 | - s3:GetObjectVersion 73 | - s3:GetBucketVersioning 74 | Resource: "*" 75 | Effect: Allow 76 | - Action: 77 | - s3:PutObject 78 | Resource: 79 | - arn:aws:s3:::codepipeline* 80 | - arn:aws:s3:::elasticbeanstalk* 81 | Effect: Allow 82 | - Action: 83 | - codecommit:CancelUploadArchive 84 | - codecommit:GetBranch 85 | - codecommit:GetCommit 86 | - codecommit:GetUploadArchiveStatus 87 | - codecommit:UploadArchive 88 | Resource: "*" 89 | Effect: Allow 90 | - Action: 91 | - codedeploy:CreateDeployment 92 | - codedeploy:GetApplicationRevision 93 | - codedeploy:GetDeployment 94 | - codedeploy:GetDeploymentConfig 95 | - codedeploy:RegisterApplicationRevision 96 | Resource: "*" 97 | Effect: Allow 98 | - Action: 99 | - elasticbeanstalk:* 100 | - ec2:* 101 | - elasticloadbalancing:* 102 | - autoscaling:* 103 | - cloudwatch:* 104 | - s3:* 105 | - sns:* 106 | - cloudformation:* 107 | - rds:* 108 | - sqs:* 109 | - ecs:* 110 | - iam:PassRole 111 | Resource: "*" 112 | Effect: Allow 113 | - Action: 114 | - lambda:InvokeFunction 115 | - lambda:ListFunctions 116 | Resource: "*" 117 | Effect: Allow 118 | - Action: 119 | - opsworks:CreateDeployment 120 | - opsworks:DescribeApps 121 | - opsworks:DescribeCommands 122 | - opsworks:DescribeDeployments 123 | - opsworks:DescribeInstances 124 | - opsworks:DescribeStacks 125 | - opsworks:UpdateApp 126 | - opsworks:UpdateStack 127 | Resource: "*" 128 | Effect: Allow 129 | CodePipeline: 130 | DependsOn: 131 | - EC2Instance 132 | Type: "AWS::CodePipeline::Pipeline" 133 | Properties: 134 | ArtifactStore: 135 | Location: !Ref ArtifactStore 136 | Type: S3 137 | RoleArn: !Sub '${CodePipelineIAMRole.Arn}' 138 | Stages: 139 | - Name: Source 140 | Actions: 141 | - Name: Source 142 | ActionTypeId: 143 | Category: Source 144 | Owner: ThirdParty 145 | Version: 1 146 | Provider: GitHub 147 | OutputArtifacts: 148 | - Name: staticwebsite 149 | Configuration: 150 | Owner: !Ref GitHubOwner 151 | Repo: !Ref GitHubRepo 152 | Branch: master 153 | OAuthToken: !Ref GitHubOAuthToken 154 | - Name: Deploy 155 | Actions: 156 | - Name: Deploy 157 | ActionTypeId: 158 | Category: Deploy 159 | Owner: AWS 160 | Version: 1 161 | Provider: CodeDeploy 162 | InputArtifacts: 163 | - Name: staticwebsite 164 | Configuration: 165 | ApplicationName: !Ref Application 166 | DeploymentGroupName: !Ref DeploymentGroup 167 | - Name: Test 168 | Actions: 169 | - Name: Test 170 | ActionTypeId: 171 | Category: Invoke 172 | Owner: AWS 173 | Version: 1 174 | Provider: Lambda 175 | Configuration: 176 | FunctionName: !Ref TestLambda 177 | UserParameters: !Sub 'http://${EC2Instance.PublicDnsName}' 178 | CodeDeployIAMRole: 179 | Type: 'AWS::IAM::Role' 180 | Properties: 181 | AssumeRolePolicyDocument: 182 | Version: '2012-10-17' 183 | Statement: 184 | - Effect: Allow 185 | Principal: 186 | Service: 187 | - 'codedeploy.amazonaws.com' 188 | Action: 189 | - 'sts:AssumeRole' 190 | Path: '/' 191 | ManagedPolicyArns: 192 | - 'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole' 193 | Application: 194 | Type: "AWS::CodeDeploy::Application" 195 | DeploymentGroup: 196 | Type: "AWS::CodeDeploy::DeploymentGroup" 197 | Properties: 198 | ApplicationName: !Ref Application 199 | Ec2TagFilters: 200 | - Key: DeploymentGroup 201 | Type: KEY_AND_VALUE 202 | Value: !Ref AWS::StackName 203 | ServiceRoleArn: !Sub '${CodeDeployIAMRole.Arn}' 204 | SecurityGroup: 205 | Type: 'AWS::EC2::SecurityGroup' 206 | Properties: 207 | GroupDescription: 'static website' 208 | VpcId: !Ref VPC 209 | SecurityGroupIngress: 210 | - FromPort: 80 211 | ToPort: 80 212 | IpProtocol: tcp 213 | CidrIp: '0.0.0.0/0' 214 | EC2InstanceProfile: 215 | Type: 'AWS::IAM::InstanceProfile' 216 | Properties: 217 | Path: '/' 218 | Roles: 219 | - !Ref EC2InstanceIAMRole 220 | EC2InstanceIAMRole: 221 | Type: 'AWS::IAM::Role' 222 | Properties: 223 | AssumeRolePolicyDocument: 224 | Version: '2012-10-17' 225 | Statement: 226 | - Effect: Allow 227 | Principal: 228 | Service: 229 | - 'ec2.amazonaws.com' 230 | Action: 231 | - 'sts:AssumeRole' 232 | Path: '/' 233 | ManagedPolicyArns: 234 | - 'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforAWSCodeDeploy' 235 | EC2Instance: 236 | Type: 'AWS::EC2::Instance' 237 | Properties: 238 | IamInstanceProfile: !Ref EC2InstanceProfile 239 | ImageId: !FindInMap [RegionMap, !Ref 'AWS::Region', AMI] 240 | InstanceType: 't2.micro' 241 | SecurityGroupIds: 242 | - !Ref SecurityGroup 243 | UserData: 244 | 'Fn::Base64': !Sub | 245 | #!/bin/bash -x 246 | yum update -y && yum install -y ruby wget && wget https://aws-codedeploy-us-east-1.s3.amazonaws.com/latest/install && chmod +x ./install && ./install auto && service codedeploy-agent start 247 | /opt/aws/bin/cfn-signal -e $? --region ${AWS::Region} --stack ${AWS::StackName} --resource EC2Instance 248 | SubnetId: !Ref Subnet 249 | Tags: 250 | - Key: DeploymentGroup 251 | Value: !Ref AWS::StackName 252 | - Key: Name 253 | Value: !Ref AWS::StackName 254 | CreationPolicy: 255 | ResourceSignal: 256 | Count: 1 257 | Timeout: PT10M 258 | LambdaIAMRole: 259 | Type: 'AWS::IAM::Role' 260 | Properties: 261 | AssumeRolePolicyDocument: 262 | Version: '2012-10-17' 263 | Statement: 264 | - Effect: Allow 265 | Principal: 266 | Service: 267 | - 'lambda.amazonaws.com' 268 | Action: 269 | - 'sts:AssumeRole' 270 | Path: '/' 271 | ManagedPolicyArns: 272 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 273 | Policies: 274 | - PolicyName: codepipeline 275 | PolicyDocument: 276 | Version: '2012-10-17' 277 | Statement: 278 | - Action: 279 | - codepipeline:PutJobFailureResult 280 | - codepipeline:PutJobSuccessResult 281 | Resource: "*" 282 | Effect: Allow 283 | TestLambda: 284 | Type: "AWS::Lambda::Function" 285 | Properties: 286 | Code: 287 | S3Bucket: 'codepipeline-codedeploy-example-lambda' 288 | S3Key: 'v2.zip' 289 | FunctionName: 'codepipeline_http_test' 290 | Handler: 'index.handler' 291 | MemorySize: 128 292 | Role: !Sub '${LambdaIAMRole.Arn}' 293 | Runtime: 'nodejs4.3' 294 | Timeout: 30 295 | Outputs: 296 | URL: 297 | Description: 'The URL pointing to the static website.' 298 | Value: !Sub 'http://${EC2Instance.PublicDnsName}' -------------------------------------------------------------------------------- /deploy/validate_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | curl -m 5 http://localhost -------------------------------------------------------------------------------- /lambda/index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var http = require('http'); 3 | 4 | exports.handler = function(event, context) { 5 | 6 | var codepipeline = new AWS.CodePipeline(); 7 | var job = event["CodePipeline.job"] 8 | 9 | var jobId = job.id; 10 | var url = job.data.actionConfiguration.configuration.UserParameters; 11 | 12 | function success(message) { 13 | codepipeline.putJobSuccessResult({ 14 | jobId: jobId 15 | }, function(err, data) { 16 | if(err) { 17 | context.fail(err); 18 | } else { 19 | context.succeed(message); 20 | } 21 | }); 22 | } 23 | 24 | function failure(message) { 25 | codepipeline.putJobFailureResult({ 26 | jobId: jobId, 27 | failureDetails: { 28 | message: JSON.stringify(message), 29 | type: 'JobFailed', 30 | externalExecutionId: context.invokeid 31 | } 32 | }, function(err, data) { 33 | if(err) { 34 | context.fail(err); 35 | } else { 36 | context.succeed(message); 37 | } 38 | }); 39 | } 40 | 41 | http.get(url, function(response) { 42 | var body = ''; 43 | response.on('data', function (chunk) { 44 | body += chunk; 45 | }); 46 | response.on('end', function () { 47 | if (response.statusCode === 200 && body.includes("

Automation for the people

")) { 48 | success("URL check successful.") 49 | } else { 50 | console.log(response.statusCode); 51 | console.log8(body); 52 | failure("Invalid status code or content."); 53 | } 54 | }); 55 | }).on('error', function(error) { 56 | failure(error); 57 | }); 58 | 59 | }; -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | DEFAULT_VPC=$(aws ec2 describe-vpcs --filter Name=isDefault,Values=true --query Vpcs[0].VpcId --output text) 4 | SUBNET=$(aws ec2 describe-subnets --filter Name=vpc-id,Values=$DEFAULT_VPC --query Subnets[0].SubnetId --output text) 5 | 6 | echo "Enter the Stack name:" 7 | read STACK_NAME 8 | 9 | echo "Enter the GitHub repository:" 10 | read GITHUB_REPO 11 | 12 | echo "Enter the GitHub owner:" 13 | read GITHUB_OWNER 14 | 15 | echo "Enter the GitHub OAuth Token:" 16 | read GITHUB_OAUTH_TOKEN 17 | 18 | aws cloudformation create-stack --stack-name $STACK_NAME --template-body file://deploy/stack.yml --parameters ParameterKey=GitHubOwner,ParameterValue=$GITHUB_OWNER ParameterKey=GitHubOAuthToken,ParameterValue=$GITHUB_OAUTH_TOKEN ParameterKey=GitHubRepo,ParameterValue=$GITHUB_REPO ParameterKey=VPC,ParameterValue=$DEFAULT_VPC ParameterKey=Subnet,ParameterValue=$SUBNET --capabilities CAPABILITY_IAM 19 | 20 | echo "Creating the CloudFormation stack, this will take a few minutes ..." 21 | 22 | aws cloudformation wait stack-create-complete --stack-name $STACK_NAME 23 | 24 | echo "Done! Send an HTTP request to the following URL, please." 25 | aws cloudformation describe-stacks --stack-name $STACK_NAME --query 'Stacks[0].Outputs[?OutputKey==`URL`].OutputValue' --output text --------------------------------------------------------------------------------