How to keep AMI up-to-date by using Packer, AWS Codebuild, and Terraform | Part 2.

How to keep AMI up-to-date by using Packer, AWS Codebuild, and Terraform | Part 2.

After the Part 1 post, which specifically explaining configuration with packer, on this part, I'll write more about Terraform and AWS Codebuild.

The final state that we'd like to have is something like this.

ami.png

2. Terraform

I will build the whole stack using Terraform. Before we start, we need a code repository to store our code. In this case, I am using Github.

Let's start setting up Codebuild !!!

  • Create a module for Codebuild

main.tf

resource "aws_codebuild_project" "main" {
  name         = var.name
  description  = var.description
  service_role = var.service_role
  artifacts {
    encryption_disabled = true
    type                = "CODEPIPELINE"
  }
  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                       = "aws/codebuild/standard:4.0"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
  }
  source {
    git_clone_depth     = 0
    insecure_ssl        = false
    report_build_status = false
    type                = "CODEPIPELINE"
    buildspec = templatefile("${path.module}/buildspec.tpl", { project = var.name })

  }
  tags = {
    ManagedBy = "Terraform"
  }
}

output.tf

output "codebuild_project_name" {
  value = aws_codebuild_project.main.name
}

variables.tf

variable "service_role" {}

variable "name" {}

variable "description" {
  default = ""
}

variable "location" {}

buldspec.tpl

version: 0.2

phases:
  pre_build:
    commands:
      - curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add -
      - apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
      - apt-get update && apt-get install packer=1.6.6
      - wget https://github.com/rgl/packer-provisioner-windows-update/releases/download/v0.10.1/packer-provisioner-windows-update_0.10.1_linux_amd64.tar.gz
      - tar -xvf packer-provisioner-windows-update_0.10.1_linux_amd64.tar.gz
      - ls -la
      - chmod +x packer-provisioner-windows-update
      - mv packer-provisioner-windows-update /usr/bin/packer-provisioner-windows-update
  build:
    commands:
      - echo Build started on `date.`
      - packer build -var-file variables.json ${project}.json
  post_build:
    commands:
      - echo Build completed on `date.`

iam.tf

locals {
  codebuild_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
  ]
}

resource "aws_iam_role_policy_attachment" "codebuild_packer_policy_attachment" {
  count      = length(local.codebuild_policy_arns)
  role       = aws_iam_role.codebuild_packer.name
  policy_arn = local.codebuild_policy_arns[count.index]
}

resource "aws_iam_role" "codebuild_packer" {
  name = "codebuild_packer_role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "codebuild.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "codebuild_packer" {
  role = aws_iam_role.codebuild_packer.name

  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Resource": [
        "*"
      ],
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "iam:PassRole",
        "iam:GetInstanceProfile"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:*"
      ],
      "Resource": [
        "${aws_s3_bucket.src.arn}",
        "${aws_s3_bucket.src.arn}/*"
      ]
    }
  ]
}
POLICY
}

###
# EC2  Role
data "aws_iam_policy_document" "ec2_default_policy" {
  statement {
    effect    = "Allow"
    resources = ["*"]

    actions = [
      "cloudwatch:GetMetricData",
      "cloudwatch:ListMetrics",
      "ec2:DescribeRegions",
      "sqs:ListQueues",
      "sts:GetCallerIdentity",
      "iam:ListAccountAliases",
      "autoscaling:CompleteLifecycleAction",
      "autoscaling:RecordLifecycleActionHeartbeat"
    ]
  }
  statement {
    effect    = "Allow"
    resources = ["*"]

    actions = [
      "s3:ListBucket",
      "s3:GetObject",
      "s3:GetBucketLocation"
    ]
  }
}

resource "aws_iam_role" "ec2_default_role" {
  name               = "packer"
  assume_role_policy = data.aws_iam_policy_document.ec2_default_sts.json
}

data "aws_iam_policy_document" "ec2_default_sts" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

locals {
  ec2_default_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
    "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
  ]
}

resource "aws_iam_role_policy_attachment" "ec2_default_attachment" {
  count      = length(local.ec2_default_policy_arns)
  role       = aws_iam_role.ec2_default_role.name
  policy_arn = local.ec2_default_policy_arns[count.index]
}
resource "aws_iam_policy" "ec2_default_policy" {
  name   = "Packer-EC2-default-Policy"
  policy = data.aws_iam_policy_document.ec2_default_policy.json
}

resource "aws_iam_role_policy_attachment" "ec2_default_policy_attachment" {
  role       = aws_iam_role.ec2_default_role.name
  policy_arn = aws_iam_policy.ec2_default_policy.arn
}

resource "aws_iam_instance_profile" "ec2_default_instance_profile" {
  name = "packer"
  role = aws_iam_role.ec2_default_role.name
}

resource "aws_iam_role" "codepipeline_role" {
  name = "codepipeline_role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "codepipeline.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "codepipeline_policy" {
  name = "codepipeline_policy"
  role = aws_iam_role.codepipeline_role.id

  policy = <<EOF
{
    "Statement": [
        {
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "*",
            "Effect": "Allow",
            "Condition": {
                "StringEqualsIfExists": {
                    "iam:PassedToService": [
                        "cloudformation.amazonaws.com",
                        "elasticbeanstalk.amazonaws.com",
                        "ec2.amazonaws.com",
                        "ecs-tasks.amazonaws.com"
                    ]
                }
            }
        },
        {
            "Action": [
                "codecommit:CancelUploadArchive",
                "codecommit:GetBranch",
                "codecommit:GetCommit",
                "codecommit:GetRepository",
                "codecommit:GetUploadArchiveStatus",
                "codecommit:UploadArchive"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codedeploy:CreateDeployment",
                "codedeploy:GetApplication",
                "codedeploy:GetApplicationRevision",
                "codedeploy:GetDeployment",
                "codedeploy:GetDeploymentConfig",
                "codedeploy:RegisterApplicationRevision"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codestar-connections:UseConnection"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "elasticbeanstalk:*",
                "ec2:*",
                "elasticloadbalancing:*",
                "autoscaling:*",
                "cloudwatch:*",
                "s3:*",
                "sns:*",
                "cloudformation:*",
                "rds:*",
                "sqs:*",
                "ecs:*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "lambda:InvokeFunction",
                "lambda:ListFunctions"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "opsworks:CreateDeployment",
                "opsworks:DescribeApps",
                "opsworks:DescribeCommands",
                "opsworks:DescribeDeployments",
                "opsworks:DescribeInstances",
                "opsworks:DescribeStacks",
                "opsworks:UpdateApp",
                "opsworks:UpdateStack"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "cloudformation:CreateStack",
                "cloudformation:DeleteStack",
                "cloudformation:DescribeStacks",
                "cloudformation:UpdateStack",
                "cloudformation:CreateChangeSet",
                "cloudformation:DeleteChangeSet",
                "cloudformation:DescribeChangeSet",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:SetStackPolicy",
                "cloudformation:ValidateTemplate"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codebuild:BatchGetBuilds",
                "codebuild:StartBuild",
                "codebuild:BatchGetBuildBatches",
                "codebuild:StartBuildBatch"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Effect": "Allow",
            "Action": [
                "devicefarm:ListProjects",
                "devicefarm:ListDevicePools",
                "devicefarm:GetRun",
                "devicefarm:GetUpload",
                "devicefarm:CreateUpload",
                "devicefarm:ScheduleRun"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "servicecatalog:ListProvisioningArtifacts",
                "servicecatalog:CreateProvisioningArtifact",
                "servicecatalog:DescribeProvisioningArtifact",
                "servicecatalog:DeleteProvisioningArtifact",
                "servicecatalog:UpdateProduct"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:ValidateTemplate"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:DescribeImages"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "states:DescribeExecution",
                "states:DescribeStateMachine",
                "states:StartExecution"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "appconfig:StartDeployment",
                "appconfig:StopDeployment",
                "appconfig:GetDeployment"
            ],
            "Resource": "*"
        }
    ],
    "Version": "2012-10-17"
}
EOF
}
  • Create Pipeline with CodePipeline

Store the whole packer config into a zip file, and store it in an S3 bucket. Please change the GitHub config with yours.

locals {
  codebuild_project = {
    dotnet-hosting = {
      name = "dotnet-hosting"
    }
  }
}
module "ami-codebuild" {
  for_each     = local.codebuild_project
  source       = "./modules/codebuild"
  name         = local.codebuild_project[each.key].name
  service_role = aws_iam_role.codebuild_packer.arn
  location     = "${aws_s3_bucket.src.bucket}/packer/packer.zip"
}

resource "aws_codepipeline" "ami" {
  name     = "ami"
  role_arn = aws_iam_role.codepipeline_role.arn
  artifact_store {
    location = aws_s3_bucket.src.bucket
    type     = "S3"
  }
  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "ThirdParty"
      provider         = "GitHub"
      version          = "1"
      output_artifacts = ["source"]

      configuration = {
        OAuthToken = var.o_auth_token
        Owner      = "uje-m"
        Repo       = "packer"
        Branch     = "main"
      }
    }
  }

  stage {
    name = "Build"

    dynamic "action" {
      for_each = local.codebuild_project
      content {
        name            = action.key
        category        = "Build"
        owner           = "AWS"
        provider        = "CodeBuild"
        version         = "1"
        input_artifacts = ["source"]
        run_order       = 1
        configuration = {
          ProjectName = module.ami-codebuild[action.key].codebuild_project_name
        }
      }
    }
  }

}

resource "aws_cloudwatch_event_rule" "every_last_sun_of_month" {
  name                = "every-last-sunday-month"
  description         = "Fires every last Sunday of the month"
  schedule_expression = "cron(0 16 ? * 1L *)"
}

resource "aws_cloudwatch_event_target" "cloudwatch_triggers_pipeline" {
  target_id = "${var.name}-commits-trigger-pipeline"
  rule      = aws_cloudwatch_event_rule.every_last_sun_of_month.name
  arn       = aws_codepipeline.ami.arn
  role_arn  = aws_iam_role.cloudwatch_ci_role.arn
}

# Allows the CloudWatch event to assume roles
resource "aws_iam_role" "cloudwatch_ci_role" {
  name_prefix = "${var.name}-cloudwatch-ci-"

  assume_role_policy = <<DOC
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "events.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
DOC
}
data "aws_iam_policy_document" "cloudwatch_ci_iam_policy" {
  statement {
    actions = [
      "iam:PassRole"
    ]
    resources = [
      "*"
    ]
  }
  statement {
    # Allow CloudWatch to start the Pipeline
    actions = [
      "codepipeline:StartPipelineExecution"
    ]
    resources = [
      aws_codepipeline.ami.arn
    ]
  }
}
resource "aws_iam_policy" "cloudwatch_ci_iam_policy" {
  name_prefix = "${var.name}-cloudwatch-ci-"
  policy      = data.aws_iam_policy_document.cloudwatch_ci_iam_policy.json
}
resource "aws_iam_role_policy_attachment" "cloudwatch_ci_iam" {
  policy_arn = aws_iam_policy.cloudwatch_ci_iam_policy.arn
  role       = aws_iam_role.cloudwatch_ci_role.name
}

I am using for_each in the module, so I would only need to add another dictionary to have more images in the pipeline.

At this stage, all configuration is complete, what I need to do is deploying it using Terraform.

Instead of using Terraform locally and managing your state somewhere, I am using Terraform Cloud.

Sign up here app.terraform.io/signup/account, configure the required variables and the AWS access key.

test.JPG