In this blog post, we’re diving into a hands-on, automated approach to provisioning and managing AWS infrastructure using AWS CodePipeline with CloudFormation templates, including nested stacks. This setup is built to support a GitOps-style deployment, allowing infrastructure to be defined, versioned, and promoted through multiple environments—Development, Staging, and Production—straight from your Git repository.
Previously, we explored CloudFormation Git Sync for standalone stacks, showcasing how changes committed to a Git repository can automatically update AWS infrastructure. Today, we’re taking that concept further by incorporating CloudFormation nested stacks, which offer a scalable, modular approach to managing complex infrastructure codebases.
Why CodePipeline?
AWS CodePipeline is a fully managed continuous integration and delivery (CI/CD) service that automates build, test, and deployment phases of your release process. With native integrations to services like CodeBuild, CloudFormation, CodeStar Connections, and GitHub, it's a great fit for managing infrastructure as code (IaC) workflows.
By connecting GitHub to CodePipeline using CodeStar Connections, and leveraging CodeBuild for validation steps like linting, we can automate a secure, repeatable, and robust infrastructure deployment process.
Architecture Overview
- Three environments: Development, Staging, and Production
- One Git repository: Contains folders representing each environment
- CloudFormation Nested Stacks: Used for modularizing common resources (like VPCs, IAM roles, and EC2 web server)
- CodePipeline: Automates deployments by detecting changes in the GitHub repo and applying the appropriate CloudFormation templates
- CodeBuild: Lints CloudFormation templates to ensure they are syntactically and structurally correct
Step 1: Create Prerequisite Components Using CloudFormation
-
GitHubConnection
: The CodeStar connection to GitHub -
PipelineArtifactStoreS3Bucket
: Stores pipeline artifacts and CloudFormation templates -
CfnlintCodeBuildProject
: Lints .yaml and .yml files in the infrastructure directory -
CodeBuildServiceRole
: Grants permissions to CodeBuild to access logs, S3, and CodeStar -
CloudFormationExecutionRole
: Used by CloudFormation to deploy stacks with permissions to access S3, IAM, and SSM -
CodePipelineRole
: Allows CodePipeline to invoke actions using the above resources
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CodeStar connection, CodePipeline for CFN stacks creation'
Parameters:
BranchName:
Type: String
Default: 'main'
FullRepositoryId:
Type: String
Default: 'chinmayto/cloudformation-gitops-with-codepipeline'
CodePipelineName:
Type: String
Default: 'webserver-from-git'
ConnectionName:
Type: String
Default: 'GitHub-to-CodePipeline'
S3BucketName:
Type: String
Default: 'ct-cfn-files-for-stack'
CodeBuildProjectName:
Type: String
Default: 'cfnlint-project'
Resources:
#####################################
# CodeStar Connection
#####################################
GitHubConnection:
Type: 'AWS::CodeStarConnections::Connection'
Properties:
ConnectionName: !Ref ConnectionName
ProviderType: 'GitHub'
#####################################
# S3 bucket for CFN nested stack templates
#####################################
PipelineArtifactStoreS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref S3BucketName
PipelineArtifactStoreS3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref PipelineArtifactStoreS3Bucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowS3AccessForPipelineServices
Principal:
Service:
- cloudformation.amazonaws.com
- codebuild.amazonaws.com
- codepipeline.amazonaws.com
Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:PutObject
- s3:ListBucket
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::${S3BucketName}/*'
- !Sub 'arn:${AWS::Partition}:s3:::${S3BucketName}'
#####################################
# CodeBuild project
#####################################
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: CodeBuildServiceRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CodeBuildBasePolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${CodeBuildProjectName}:*'
- Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:PutObject
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::${S3BucketName}/*'
- Effect: Allow
Action:
- codeconnections:GetConnectionToken
Resource: !GetAtt GitHubConnection.ConnectionArn
CfnlintCodeBuildProject:
Type: 'AWS::CodeBuild::Project'
Properties:
Name: !Ref CodeBuildProjectName
Description: 'Project to run cfn-lint on the source code'
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:5.0
EnvironmentVariables: []
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
install:
commands:
- echo "Installing CloudFormation Linter:"
- pip install cfn-lint --user
build:
commands:
- echo "Running linter on infrastructure directory:"
- |
ERR=0
for file in $(find ./infrastructure -type f \( -iname "*.yaml" -o -iname "*.yml" \)); do
cfn-lint "$file" || ERR=1
done
if [ "$ERR" -eq "1" ]; then
exit 1
fi
artifacts:
files:
- '**/*'
ServiceRole: !Ref CodeBuildServiceRole
#####################################
# CodePipeline pipeline
#####################################
CloudFormationExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: CloudFormationExecutionRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- cloudformation.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: !Sub "CloudFormationDeploymentPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ec2:*
- autoscaling:*
- iam:PassRole
- iam:GetRole
- iam:CreateInstanceProfile
- iam:AddRoleToInstanceProfile
- iam:RemoveRoleFromInstanceProfile
- iam:DeleteInstanceProfile
- iam:CreateRole
- iam:PutRolePolicy
- iam:AttachRolePolicy
- iam:ListInstanceProfiles
- iam:ListRoles
- iam:DeleteRolePolicy
- iam:TagRole
- iam:DeleteRole
- iam:GetInstanceProfile
- iam:getRolePolicy
- ssm:GetParameter
- ssm:GetParameters
- logs:*
- cloudwatch:PutMetricData
- cloudformation:*
- s3:ListBucket
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: "*"
- PolicyName: CloudFormationPassRolePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:PassRole
Resource: !Sub 'arn:aws:iam::197317184204:role/CloudFormationExecutionRole'
CodePipelineRole:
Type: AWS::IAM::Role
Properties:
RoleName: CodePipelineRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CodeStarSourceConnectionAccessPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- codestar-connections:UseConnection
Resource: !Sub 'arn:${AWS::Partition}:codestar-connections:${AWS::Region}:${AWS::AccountId}:connection/*'
- PolicyName: CodeBuildPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
Resource: !Sub 'arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CfnlintCodeBuildProject}'
- Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:PutObject
Resource: !Sub 'arn:${AWS::Partition}:s3:::${S3BucketName}/*'
- Effect: Allow
Action:
- s3:ListBucket
Resource: !Sub 'arn:${AWS::Partition}:s3:::${S3BucketName}'
- Effect: Allow
Action:
- s3:ListBucket
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::${S3BucketName}'
- PolicyName: CodeDeployPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- cloudformation:CreateStack
- cloudformation:DeleteStack
- cloudformation:DescribeStacks
- cloudformation:UpdateStack
- cloudformation:DescribeStackEvents
- cloudformation:SetStackPolicy
- cloudformation:ValidateTemplate
Resource: '*'
- PolicyName: CodePipelinePassRolePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:PassRole
Resource:
- !GetAtt CloudFormationExecutionRole.Arn
- !GetAtt CodeBuildServiceRole.Arn
Condition:
StringEqualsIfExists:
iam:PassedToService:
- cloudformation.amazonaws.com
- codebuild.amazonaws.com
CreateCfnStackFromRepo:
Type: 'AWS::CodePipeline::Pipeline'
Properties:
Name: !Ref CodePipelineName
RoleArn: !GetAtt CodePipelineRole.Arn
ArtifactStore:
Type: S3
Location: !Ref S3BucketName
Stages:
- Name: Source
Actions:
- Name: Source
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeStarSourceConnection
Version: '1'
RunOrder: 1
Configuration:
BranchName: !Ref BranchName
ConnectionArn: !GetAtt GitHubConnection.ConnectionArn
DetectChanges: 'true'
FullRepositoryId: !Ref FullRepositoryId
OutputArtifactFormat: CODE_ZIP
OutputArtifacts:
- Name: SourceArtifact
Namespace: SourceVariables
- Name: CFN-Lint
Actions:
- Name: Run-CFN-Lint
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
Configuration:
ProjectName: !Ref CfnlintCodeBuildProject
InputArtifacts:
- Name: SourceArtifact
OutputArtifacts:
- Name: CflintArtifact
RunOrder: 1
- Name: Copy-to-S3
Actions:
- Name: Copy-to-S3
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: S3
Version: '1'
RunOrder: 1
Configuration:
BucketName: !Ref S3BucketName
Extract: 'true'
InputArtifacts:
- Name: SourceArtifact
- Name: Deploy-CFN-stacks
Actions:
- Name: DeployDevelopmentStack
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CREATE_UPDATE
Capabilities: 'CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND'
StackName: !Sub '${CodePipelineName}-development'
TemplatePath: SourceArtifact::infrastructure/development/root.yaml
RoleArn: !GetAtt CloudFormationExecutionRole.Arn
ParameterOverrides: |
{
"Environment": "development"
}
InputArtifacts:
- Name: SourceArtifact
RunOrder: 1
- Name: DeployStagingStack
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CREATE_UPDATE
Capabilities: 'CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND'
StackName: !Sub '${CodePipelineName}-staging'
TemplatePath: SourceArtifact::infrastructure/staging/root.yaml
RoleArn: !GetAtt CloudFormationExecutionRole.Arn
ParameterOverrides: |
{
"Environment": "staging"
}
InputArtifacts:
- Name: SourceArtifact
RunOrder: 1
- Name: DeployProductionStack
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CREATE_UPDATE
Capabilities: 'CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND'
StackName: !Sub '${CodePipelineName}-production'
TemplatePath: SourceArtifact::infrastructure/production/root.yaml
RoleArn: !GetAtt CloudFormationExecutionRole.Arn
ParameterOverrides: |
{
"Environment": "production"
}
InputArtifacts:
- Name: SourceArtifact
RunOrder: 1
You can apply this template using a simple shell script like below:
#!/bin/bash
# This script deploys a CloudFormation stack
STACK_NAME="codepipeline-pipeline-cfn"
echo "Deploying CloudFormation stack: $STACK_NAME"
aws cloudformation deploy \
--stack-name $STACK_NAME \
--template-file codepipeline_pipeline.yaml \
--capabilities CAPABILITY_NAMED_IAM --disable-rollback
if [ $? -eq 0 ]; then
echo "CloudFormation stack $STACK_NAME deployed successfully."
else
echo "Failed to deploy CloudFormation stack $STACK_NAME."
exit 1
fi
# Wait for the stack to be created
aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME"
if [ $? -eq 0 ]; then
echo "Stack $STACK_NAME creation completed successfully."
else
echo "Failed to create stack $STACK_NAME."
exit 1
fi
Run the shell script:
$ ./cfn-deploy-pipeline.sh
Deploying CloudFormation stack: codepipeline-pipeline-cfn
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - codepipeline-pipeline-cfn
CloudFormation stack codepipeline-pipeline-cfn deployed successfully.
Stack codepipeline-pipeline-cfn creation completed successfully.
Step 2: Authorize GitHub in CodeStar Connection
Once the connection is created, go back to the Connections tab in the AWS console and authorize GitHub access.
At times, if you had previously linked your repository to your AWS account using a CodeStar connection, deleting and recreating the connection might still cause issues when creating a new CloudFormation stack—AWS may continue referencing the "old" connection. To resolve this, you should unlink the repository using the AWS CLI and then link it again to refresh the connection. Make sure to authorize again via the console after creating a new connection.
List connection
aws codestar-connections list-repository-links
Delete repository link
aws codestar-connections delete-repository-link --repository-link-id ac01d54c-dcc7-4b4e-97bf-f70592f1377d
Step 3: Watch Initial Pipeline Run
Once the stack is deployed and the pipeline is created, CodePipeline automatically starts an initial run:
- It detects changes from the specified GitHub branch
- Downloads the templates
- Runs cfn-lint via CodeBuild
- Deploys nested stacks using CloudFormation
Build: cloudformation lint - cfn-lint:
Deploy: Infra provisioning in progress:
Deploy: Infra provisioning in complete:
Step 4: Make Changes and Watch Them Deploy
With the GitOps model in place, any change committed to the GitHub repo will trigger the pipeline. For example:
Lets update the desired capacity of autoscaling group to 3 for development environment.
to
Commit the changes to github repo and watch your infra getting updated accordingly.
Infra updation in progress and complete:
Additional development instances:
Cleanup
When you are done testing or no longer need the stacks, delete them manually via the AWS Console or CLI
aws cloudformation delete-stack --stack-name webserver-from-git-development
aws cloudformation delete-stack --stack-name webserver-from-git-staging
aws cloudformation delete-stack --stack-name webserver-from-git-production
aws cloudformation delete-stack --stack-name codepipeline-pipeline-cfn
Conclusion
By combining CloudFormation nested stacks with CodePipeline and GitHub, we've created a robust automation pipeline that supports infrastructure deployments across multiple environments. This solution builds upon the GitOps paradigm, enabling a safer, more auditable way to manage AWS infrastructure at scale.
This approach not only improves deployment consistency but also integrates seamlessly with developer workflows—making infrastructure provisioning as easy as a git push.
References
GitHub Repo: https://github.com/chinmayto/cloudformation-gitops-with-codepipeline
How to model GitOps environments: https://codefresh.io/blog/
how-to-model-your-gitops-environments-and-promote-releases-between-them/
Top comments (0)