Anything with Cloudformation

As part of the DevOps Masters Program on Simplilearn, had to configure a jenkins pipeline. For the same, even though they do provide a lab environment, I feel at home with AWS and cli. I myself being part of the AWS Community Builders, should normally prefer this approach.

For the particular project, the infrastructure was visualized by me as two AWS::EC2 pre deployed one for Jenkins master node, and the other for java+tomcat to deploy a sample app. The Jenkins would be configured with Cloud Plugin configured to manage EC2 nodes for build and test and finally deploy to the tomcat using remote deployment using war. Making the long story short lets jump straight into the steps. Agree that I completed the Project Run in about a couple of hours and creating such a template and running through aws-sam was purely on academic interest. Download the template file: cf-template-ec2-jenkins-tomcat-ubuntu-bionic.yaml

I think the template could be explained section by section as follows

Parameters:
  # This is a security group which permits all ports from my ip only
  DevopsFireWall:
    Description: The DevOps Firewall Security Group ID
    Type: AWS::EC2::SecurityGroup::Id 
  # ID of an existing VPC to create internal firewall and deploy the machines  
  TheVPC:
    Description: ID of An existing VPC
    Type: AWS::EC2::VPC::Id   
  # AMI id - this is ubuntu 18.04 on ap-south-1  
  ImageID:
    Description: AMI id preferred to use ubuntu 18.04
    Type: AWS::EC2::Image::Id
  # Intstance type ( defines memory and processor )  
  InstanceSize:
    Description: Instance Size
    Type: String
    Default: c5.large
  # SSH key pair, will need to ssh into sometimes  
  KeyPair:
    Description: Key Pair Name - Should be existing key pair
    Type: String

The template is parameterized, such that it can be made flexible, like when the template was initially tested, I used t2.medium for the instances and later in the final run the instance was switched to m5.large. When running one should make sure the image is ubuntu. I always create a primary security group with blanket permission from my IP address. This is achieved using some of my snippets, and some custom command-line tools. So I use this security group id for the first parameter (DevopsFireWall). Also I will have a pre-generated ssh key-pair which is supplied to the last parameter (KeyPair).


  # create an internal firewall - security group
  FireWallInternal:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Internal All Traffic Group 
      VpcId: !Ref TheVPC

  # this rule opens all ports and protocols with source 
  # as the same security group when multiple ec2 is attached
  # they can communicate internally without issues
  FireWallRuleInternal:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: "-1"
      SourceSecurityGroupId:
        Fn::GetAtt:
        - FireWallInternal
        - GroupId
      GroupId:
        Fn::GetAtt:
        - FireWallInternal
        - GroupId

This section actually creates a different security group and adds an inbound rule with source group id as self and fully open connections from and to any port, which when attached to multiple ec2 machines, they can communicate internally using the private ip addresses.

  # the EC2 machine role, which is the recommended practice to provide access to aws services internally
  MachineRole: 
    Type: "AWS::IAM::Role"
    Properties: 
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - 
            Effect: "Allow"
            Principal: 
              Service: 
                - "ec2.amazonaws.com"
            Action: 
              - "sts:AssumeRole"
      Path: "/"
      Policies:
        - PolicyName: CFN2StackAccess
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: 
                  - "cloudformation:SignalResource"
                  - "cloudformation:DescribeStackResource"
                Resource: !Sub "${AWS::StackId}"

  # the EC2 Access Profile which utilizes the above machine role
  EC2AccessProfile: 
    Type: "AWS::IAM::InstanceProfile"
    Properties: 
      Path: "/"
      Roles: 
        - 
          Ref: "MachineRole"

The MachineRole, which is a role with AssumeRole permissions for EC2 and an inline policy with cloudformation actions SignalResource and DescribeStackResource. These permissions were added to the Role Policy after reading through some notes on aws blog. But actually I don’t think it is necessary as it is written there “if no credentials are specified, CloudFormation checks for stack membership and limits the scope of the call to the stack that the instance belongs to. But this was not tested without the permissions, hence it is left there as is.

  JenkinsEC2:
    Type: AWS::EC2::Instance
    CreationPolicy:
      ResourceSignal:
        Timeout: PT30M    
    Properties:
      ImageId: !Ref ImageID
      KeyName: !Ref KeyPair 
      InstanceType: !Ref InstanceSize
      IamInstanceProfile: !Ref EC2AccessProfile
      SecurityGroupIds:
        - !Ref DevopsFireWall
        - !Ref FireWallInternal
      UserData: 
        Fn::Base64: 
          Fn::Sub: |
            #!/bin/bash
            wget -q https://www.jijutm.com/downloads/jenkins-install.sh -O /tmp/install.sh
            . /tmp/install.sh
            /usr/local/bin/cfn-signal --exit-code 0 --resource JenkinsEC2 --region ${AWS::Region} --stack ${AWS::StackName}

Simply bare minimum for the EC2 for Jenkins Master. The installation file jenkins-install.sh does the whole magic, but till date I could not find an alternative way to complete the full installation and activation from command-line. Might investigate a possibility with curl or selenium-firefox as there is a randomly generated password which is written to a file and has to be fetched using ssh.

  TomcatEC2:
    Type: AWS::EC2::Instance
    CreationPolicy:
      ResourceSignal:
        Timeout: PT30M    
    Properties:
      ImageId: !Ref ImageID
      KeyName: !Ref KeyPair 
      InstanceType: !Ref InstanceSize
      IamInstanceProfile: !Ref EC2AccessProfile
      SecurityGroupIds:
        - !Ref DevopsFireWall
        - !Ref FireWallInternal
      UserData: 
        Fn::Base64: 
          Fn::Sub: |
            #!/bin/bash
            wget -q https://www.jijutm.com/downloads/tomcat-install.sh -O /tmp/install.sh
            . /tmp/install.sh
            /usr/local/bin/cfn-signal --exit-code 0 --resource TomcatEC2 --region ${AWS::Region} --stack ${AWS::StackName}

Similar minimal EC2 for the Tomcat Server, with the magic inside tomcat-install.sh. Glad that the How To Install Apache Tomcat 9 on Ubuntu 18.04 article on DigitalOcean was right on the topic and gave me the whole copy-paste commands which I used inside the cloud-init.

  # EIP (Static IP) for both machines
  JenkinsEIP:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref JenkinsEC2     

  TomcatEIP:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref TomcatEC2   

Here we assign two EIP and assign one each to the EC2 instances. This is to make sure we can configure and do the rest of the project at leisure. In between we can shutdown the instances and keep them stopped, without loosing the ip. Also it would be easy to access the instances using the public dns names (by aws) since the same will resolve to the EIP when queried outside and the private ip when queried inside. This helps the jenkins to communicate with tomcat using the same public dns name. Hence the outputs configured were only the public dns names

Outputs:
  JenkinsIP:
    Description: "Jenkins Public DNS Name"
    Value: !GetAtt JenkinsEC2.PublicDnsName
  TomcatIP:
    Description: "Tomcat Public DNS Name"
    Value: !GetAtt TomcatEC2.PublicDnsName

For both the instances, a CreationPolicy with ResourceSignal Timeout of 30 Minutes were given, being lazy, just let the build run and will check back only later. Also the MachineRole (IamInstanceProfile), KeyName, SecurityGroup list were close to the same. The tomcat configuration is wide open without any security other than the AWS Security Group. So be careful on the ingress rules.

This template was ran from command line using aws-sam. Well this is just a personal preference as I find command line much more efficient and easy. Ok enough of the crap. Will flow into the cli screenshots.

sam depploy

Well though the second screenshot was a time-lapse updated display, the finals was taken as a screenshot.

Once again this is not secure enough for a production run, where one can experiment with AWS WAF for ALB in front of the Tomcat. This could also enable SSL for the application on tomcat. Again the application could be autoscaled with further tweaks. Offhand with AWS the options are unlimited.