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

Infrastructure Developer
Search for a command to run...

Infrastructure Developer
No comments yet. Be the first to comment.
Prerequisite Install AWS Load Balancer Controller: This controller is essential for managing AWS load balancers in a Kubernetes environment. Step 1: Enable Proxy Protocol in NLB Target Group Add the following annotation to your NLB Target Group to...

AWS Managed NAT Gateway is a widely-used service that provides seamless internet access for resources within a private VPC subnet. However, its cost structure has been a pain point for many AWS users, with high fees for both hourly usage and data pro...
Configure NTP in timesyncd.conf echo "NTP=169.254.169.123" | sudo tee -a /etc/systemd/timesyncd.conf cat /etc/systemd/timesyncd.conf Enable the timesyncd.service sudo systemctl unmask systemd-timesyncd.service sudo apt install systemd-timesyncd ...

Error waiting to create Instance: Error waiting for Creating Instance: Error code 7, message: Required 'compute.subnetworks.use' permission for

automating time tracking for a more accurate record of time spent on projects

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.

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 !!!
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
}
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 https://app.terraform.io/signup/account, configure the required variables and the AWS access key.
