DEV Community

Cover image for Automating AWS Infrastructure Provisioning with CodePipeline and CloudFormation Nested Stacks

Automating AWS Infrastructure Provisioning with CodePipeline and CloudFormation Nested Stacks

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

Image description

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Image description

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

Delete repository link

aws codestar-connections delete-repository-link --repository-link-id ac01d54c-dcc7-4b4e-97bf-f70592f1377d
Enter fullscreen mode Exit fullscreen mode

Step 3: Watch Initial Pipeline Run

Once the stack is deployed and the pipeline is created, CodePipeline automatically starts an initial run:

  1. It detects changes from the specified GitHub branch
  2. Downloads the templates
  3. Runs cfn-lint via CodeBuild
  4. Deploys nested stacks using CloudFormation

Build: cloudformation lint - cfn-lint:
Image description

Deploy: Infra provisioning in progress:
Image description

Image description

Deploy: Infra provisioning in complete:
Image description

Image description

Image description

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.

Image description

to

Image description

Commit the changes to github repo and watch your infra getting updated accordingly.

Infra updation in progress and complete:
Image description

Additional development instances:
Image description

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
Enter fullscreen mode Exit fullscreen mode

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)