AWS and CloudFormation

I have recently been learning Terraform in order to deploy code into AWS, with and without pipelines for CI/CD. I was asked to create an entire infrastructure with CloudFormation which i had never used before. I have since been able to create and deploy multiuple stacks using CodePipeline to ensure it gets updated after any change to the CodeCommit repo.

Main CloudFormation Stack

Once a main CloudFormation stack has been defined, you can then start pointing each stack to the individual template files.

AWSTemplateFormatVersion: '2010-09-09'

Description: Master stack which creates all required nested stacks

Parameters:
 vpccidr: 
      Description: "The VPC CIDR address range for the VPC to be deployed."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
 publicsubnet1: 
      Description: "The CIDR address range for public subnet 1."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
 publicsubnet2: 
      Description: "The CIDR address range for public subnet 2."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
 privatesubnet1: 
      Description: "The CIDR address range for private subnet 1."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
 privatesubnet2: 
      Description: "The CIDR address range for private subnet 2."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
Resources:
  VPCStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "https://S3_Bucket_Location/nwsis/vpc-stack.yml"
      Parameters:
        vpccidr: !Ref vpccidr
        publicsubnet1: !Ref publicsubnet1
        publicsubnet2: !Ref publicsubnet2
        privatesubnet1: !Ref privatesubnet1
        privatesubnet2: !Ref privatesubnet2
      Tags:
        - Key: Owner
          Value: Owner_Name
        - Key: Environment
          Value: !Ref environment
        - Key: Criticality
          Value: !Ref criticality
        - Key: Application
          Value: Application Name

This main yaml file will then point to the vpc-stack.yml file in order to create the VPC infrastructure. The Ref function calls the value from the parameters sepcified above. The parameters in turn get the values from a JSON encoded file that holds the values that you want to create; including CIDR ranges for the subnets.

Stack Template File

AWSTemplateFormatVersion: '2010-09-09'
Description: "Creation of VPC and SubNets"
Parameters:
 vpccidr: 
      Description: "The VPC CIDR address range for the VPC to be deployed."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
 publicsubnet1: 
      Description: "The CIDR address range for public subnet 1."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
 publicsubnet2: 
      Description: "The CIDR address range for public subnet 2."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
 privatesubnet1: 
      Description: "The CIDR address range for private subnet 1."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
 privatesubnet2: 
      Description: "The CIDR address range for private subnet 2."
      Type: String
      MinLength: '9'
      MaxLength: '18'
      AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
      ConstraintDescription: "Must be a valid IP CIDR range of the form x.x.x.x/x."
Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketEncryption:
         ServerSideEncryptionConfiguration:
            - ServerSideEncryptionByDefault:
                SSEAlgorithm: 'aws:kms'
                KMSMasterKeyID: !Ref EncryptionKey
              BucketKeyEnabled: true
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 
        Ref: vpccidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: VPC-NAME
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 
        Ref: publicsubnet1
      MapPublicIpOnLaunch: false
      VpcId:
        Ref: VPC
      Tags:
        - Key: "Name"
          Value: PublicSubnet1
      AvailabilityZone: !Sub ${AWS::Region}a
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 
        Ref: publicsubnet2
      MapPublicIpOnLaunch: false
      VpcId:
        Ref: VPC
      Tags:
        - Key: "Name"
          Value: PublicSubnet2
      AvailabilityZone: !Sub ${AWS::Region}b
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 
        Ref: privatesubnet1
      MapPublicIpOnLaunch: false
      VpcId:
        Ref: VPC
      Tags:
        - Key: "Name"
          Value: PrivateSubnet1
      AvailabilityZone: !Sub ${AWS::Region}a
  PrivateSubnet2:
    Type: "AWS::EC2::Subnet"
    Properties:
      CidrBlock: 
        Ref: privatesubnet2
      MapPublicIpOnLaunch: false
      VpcId:
        Ref: "VPC"
      Tags:
        - Key: "Name"
          Value: PrivateSubnet2
      AvailabilityZone: !Sub ${AWS::Region}b
 EncryptionKey:
    Type: AWS::KMS::Key
    Properties:
      Description: 'This is the key used to secure resources in this account'
      EnableKeyRotation: True
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: Allow root access
            Effect: 'Allow'
            Principal:
              AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
            Action:
              - 'kms:*'
            Resource: '*'
          - Sid: Allow use of the key by this account
            Effect: 'Allow'
            Principal:
              AWS: '*'
            Action:
              - 'kms:DescribeKey'
              - 'kms:Encrypt'
              - 'kms:Decrypt'
              - 'kms:ReEncrypt*'
              - 'kms:GenerateDataKey'
              - 'kms:GenerateDataKeyWithoutPlaintext'
              - 'kms:CreateGrant'
              - 'kms:RevokeGrant'
            Resource: '*'
            Condition:
              StringEquals:
                kms:CallerAccount: !Ref 'AWS::AccountId'
  EncryptionKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: alias/encryptionKey
      TargetKeyId: !Ref EncryptionKey
 Outputs:
  S3BucketName:
    Description: "Name of the S3 bucket"
    Value: !Ref S3Bucket
  vpc:
    Description: "VPC ID"
    Value: !Ref VPC
  PrivateSubnet1:
    Description: "Subnet ID of private subnet in AZ1"
    Value: !Ref PrivateSubnet1
  PrivateSubnet2:
    Description: "Subnet ID of private subnet in AZ2"
    Value: !Ref PrivateSubnet2
  PublicSubnet1:
    Description: "Subnet ID of public subnet in AZ1"
    Value: !Ref PublicSubnet1
  PublicSubnet2:
    Description: "Subnet ID of public subnet in AZ2"
    Value: !Ref PublicSubnet2

Once the files are created you can then push to your repo. As long as your pipeline is pointing at your repo and configured to the correct branch, it should start deploying the cloud formation scripts. The error messages are very good and you can add as many nested stacks as you need to deploy all your environment.

git add .

git commit -m "Created the VPC Stack"

git push