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.
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
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 "codebuild_project_name" {
value = aws_codebuild_project.main.name
}
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.`
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.