In the world of cloud-native applications, automation is crucial to streamline deployments and manage infrastructure efficiently. In this article, we’ll explore how to automate the setup of an EKS cluster and deploy a Flask application using Terraform and GitHub Actions. We’ll also touch on security best practices, monitoring, and how to ensure a robust CI/CD pipeline.
Table of Contents
Overview
Prerequisites
Infrastructure Automation: EKS Cluster Setup
Application Deployment: Flask App on EKS
Monitoring with Prometheus and Grafana
Security Best Practices
Conclusion
Overview
The goal of this project is to automate the deployment of a containerised Flask application on an EKS (Elastic Kubernetes Service) cluster. Using Terraform to provision AWS resources and GitHub Actions to automate the CI/CD pipeline, this setup allows for seamless infrastructure management and application deployment.
Why Terraform?
Terraform enables you to write declarative code for infrastructure. Instead of manually creating resources like VPCs, subnets, or an EKS cluster, we automate everything via Infrastructure as Code (IaC).
Why GitHub Actions?
GitHub Actions provides a powerful way to integrate CI/CD, testing, static analysis, and security checks into the code deployment process.
Prerequisites
Before diving into the automation, here are the prerequisites you’ll need to get started:
AWS Account: Create an AWS account if you don’t have one.
IAM Access Keys: Set up access keys with permissions for managing EKS, EC2, and S3.
S3 Bucket: Create an S3 bucket to store your Terraform state files securely.
AWS CLI: Install and configure the AWS CLI.
Terraform: Make sure Terraform is installed on your local machine or use GitHub Actions for automation.
GitHub Secrets: Add AWS credentials (access keys, secret keys) and other sensitive data as GitHub secrets to avoid hardcoding them.
Synk: Create a Synk account and get your Token.
SonarCloud: Create a SonarCloud account and get your Token, Organisation key and Project key.
Infrastructure Automation: EKS Cluster Setup
Automating infrastructure deployment is key to maintaining scalable, consistent, and reliable environments. In this project, Terraform is utilised to automate the provisioning of an EKS cluster, its node groups, and the supporting AWS infrastructure. This includes VPC creation, IAM roles, S3 bucket setup, and cloud resources like CloudWatch and CloudTrail for logging and monitoring.
Terraform Setup
Let’s start by provisioning the necessary infrastructure. Below is the detailed explanation of the key resources defined in the Terraform files.
EKS Cluster and Node Group (main.tf):
This provision an EKS cluster and node group with IAM roles attached.
The cluster supports encryption using a KMS key, and the worker nodes are set up to scale between a minimum of 2 nodes. Outputs include the cluster name and endpoint for easy reference.
touch main.tf
terraform {
backend "s3" {
bucket = "regtech-iac"
key = "terraform.tfstate"
region = "us-east-1"
encrypt = true
}
}
# Provides an EKS Cluster
resource "aws_eks_cluster" "eks_cluster" {
name = var.cluster_name
role_arn = aws_iam_role.eks_cluster_role.arn
version = "1.28"
vpc_config {
subnet_ids = [aws_subnet.public_subnet_1.id, aws_subnet.public_subnet_2.id, aws_subnet.public_subnet_3.id]
}
encryption_config {
provider {
key_arn = aws_kms_key.eks_encryption_key.arn
}
resources = ["secrets"]
}
# Ensure that IAM Role permissions are created before and deleted after EKS Cluster handling.
# Otherwise, EKS will not be able to properly delete EKS managed EC2 infrastructure such as Security Groups.
depends_on = [
aws_iam_role_policy_attachment.eks_cluster_policy_attachment,
aws_iam_role_policy_attachment.eks_service_policy_attachment,
]
}
# Provides an EKS Node Group
resource "aws_eks_node_group" "eks_node_group" {
cluster_name = aws_eks_cluster.eks_cluster.name
node_group_name = var.node_group_name
node_role_arn = aws_iam_role.eks_node_group_role.arn
subnet_ids = [aws_subnet.public_subnet_1.id, aws_subnet.public_subnet_2.id, aws_subnet.public_subnet_3.id]
scaling_config {
desired_size = 2
max_size = 2
min_size = 2
}
update_config {
max_unavailable = 1
}
# Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling.
# Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces.
depends_on = [
aws_iam_role_policy_attachment.eks_worker_node_policy_attachment,
aws_iam_role_policy_attachment.eks_cni_policy_attachment,
aws_iam_role_policy_attachment.ec2_container_registry_readonly,
]
}
# Extra resources
resource "aws_ebs_volume" "volume_regtech"{
availability_zone = var.az_a
size = 40
encrypted = true
type = "gp2"
kms_key_id = aws_kms_key.ebs_encryption_key.arn
}
resource "aws_s3_bucket" "regtech_iac" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_server_side_encryption_configuration" "regtech_iac_encrypt_config" {
bucket = aws_s3_bucket.regtech_iac.bucket
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.s3_encryption_key.arn
sse_algorithm = "aws:kms"
}
}
}
# OutPut Resources
output "endpoint" {
value = aws_eks_cluster.eks_cluster.endpoint
}
output "eks_cluster_name" {
value = aws_eks_cluster.eks_cluster.name
}
Networking (vpc.tf):
Defines a VPC, public subnets for the EKS cluster, and private subnets for other resources, ensuring flexibility in network architecture.
vpc.tf
# Provides a VPC resource
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr_block
instance_tenancy = "default"
tags = {
Name = var.tags_vpc
}
}
# Provides an VPC Public subnet resource
resource "aws_subnet" "public_subnet_1" {
vpc_id = aws_vpc.main.id
cidr_block = var.p_s_1_cidr_block
availability_zone = var.az_a
map_public_ip_on_launch = true
tags = {
Name = var.tags_public_subnet_1
}
}
resource "aws_subnet" "public_subnet_2" {
vpc_id = aws_vpc.main.id
cidr_block = var.p_s_2_cidr_block
availability_zone = var.az_b
map_public_ip_on_launch = true
tags = {
Name = var.tags_public_subnet_2
}
}
resource "aws_subnet" "public_subnet_3" {
vpc_id = aws_vpc.main.id
cidr_block = var.p_s_3_cidr_block
availability_zone = var.az_c
map_public_ip_on_launch = true
tags = {
Name = var.tags_public_subnet_3
}
}
# Provides an VPC Private subnet resource
resource "aws_subnet" "private_subnet_1" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_s_1_cidr_block
availability_zone = var.az_private_a
map_public_ip_on_launch = false
tags = {
Name = var.tags_private_subnet_1
}
}
resource "aws_subnet" "private_subnet_2" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_s_2_cidr_block
availability_zone = var.az_private_b
map_public_ip_on_launch = false
tags = {
Name = var.tags_private_subnet_2
}
}
resource "aws_subnet" "private_subnet_3" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_s_3_cidr_block
availability_zone = var.az_private_c
map_public_ip_on_launch = false
tags = {
Name = var.tags_private_subnet_3
}
}
IAM Roles (iam.tf):
IAM roles and policies for the EKS cluster, node groups, and autoscaler. Includes roles for security services like CloudWatch and CloudTrail, ensuring robust monitoring.
iam.tf
# Declare the aws_caller_identity data source
data "aws_caller_identity" "current" {}
# IAM Role for EKS Cluster Plane
resource "aws_iam_role" "eks_cluster_role" {
name = var.eks_cluster_role_name
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "eks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy_attachment" "eks_cluster_policy_attachment" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
role = aws_iam_role.eks_cluster_role.name
}
resource "aws_iam_role_policy_attachment" "eks_service_policy_attachment" {
role = aws_iam_role.eks_cluster_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSServicePolicy"
}
# IAM Role for Worker node
resource "aws_iam_role" "eks_node_group_role" {
name = var.eks_node_group_role_name
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy_attachment" "eks_worker_node_policy_attachment" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
role = aws_iam_role.eks_node_group_role.name
}
resource "aws_iam_role_policy_attachment" "eks_cni_policy_attachment" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
role = aws_iam_role.eks_node_group_role.name
}
resource "aws_iam_role_policy_attachment" "ec2_container_registry_readonly" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
role = aws_iam_role.eks_node_group_role.name
}
resource "aws_iam_instance_profile" "eks_node_instance_profile" {
name = var.eks_node_group_profile
role = aws_iam_role.eks_node_group_role.name
}
# Policy For volume creation and attachment
resource "aws_iam_role_policy" "eks_node_group_volume_policy" {
name = var.eks_node_group_volume_policy_name
role = aws_iam_role.eks_node_group_role.name
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:CreateTags",
"ec2:DescribeTags",
"ec2:DescribeVolumes",
"ec2:DescribeVolumeStatus",
"ec2:CreateVolume",
"ec2:AttachVolume"
],
"Resource": "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:volume/*"
}
]
})
}
# IAM Role for CloudWatch
resource "aws_iam_role" "cloudwatch_role" {
name = "cloudwatch_role_log"
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "cloudwatch.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
})
}
resource "aws_iam_role_policy_attachment" "cloudwatch_policy_attachment" {
role = aws_iam_role.cloudwatch_role.name
policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
}
# IAM Role for CloudTrail
resource "aws_iam_role" "cloudtrail_role" {
name = "cloudtrail_role_log"
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "cloudtrail.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
})
}
resource "aws_iam_role_policy_attachment" "cloudtrail_policy_attachment" {
role = aws_iam_role.cloudtrail_role.name
policy_arn = "arn:aws:iam::aws:policy/AWSCloudTrail_FullAccess"
}
# KMS Key Policy for Encryption
resource "aws_kms_key" "ebs_encryption_key" {
description = "KMS key for EBS volume encryption"
}
resource "aws_kms_key" "s3_encryption_key" {
description = "KMS key for S3 bucket encryption"
}
resource "aws_kms_key" "eks_encryption_key" {
description = "KMS key for EKS secret encryption"
}
resource "aws_s3_bucket_policy" "regtech_iac_policy" {
bucket = aws_s3_bucket.regtech_iac.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = "s3:GetBucketAcl"
Resource = "arn:aws:s3:::${aws_s3_bucket.regtech_iac.bucket}"
},
{
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = "s3:PutObject"
Resource = "arn:aws:s3:::${aws_s3_bucket.regtech_iac.bucket}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
Condition = {
StringEquals = {
"s3:x-amz-acl" = "bucket-owner-full-control"
}
}
}
]
})
}
CloudWatch and Monitoring (cloudwatch.tf):
This provisions CloudWatch log groups, an SNS topic for alerts, and a CloudWatch alarm to monitor CPU utilisation. CloudTrail logs are configured to monitor S3 and management events.
touch cloudwatch.tf
resource "aws_cloudwatch_log_group" "eks_log_group" {
name = "/aws/eks/cluster-logs-regtech"
retention_in_days = 30
}
resource "aws_cloudtrail" "security_trail" {
name = "security-trail-log"
s3_bucket_name = aws_s3_bucket.regtech_iac.bucket
include_global_service_events = true
is_multi_region_trail = true
enable_log_file_validation = true
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::${aws_s3_bucket.regtech_iac.bucket}/"]
}
}
}
resource "aws_sns_topic" "alarm_topic" {
name = "high-cpu-alarm-topic"
}
resource "aws_sns_topic_subscription" "alarm_subscription" {
topic_arn = aws_sns_topic.alarm_topic.arn
protocol = "email"
endpoint = "oloruntobiolurombi@gmail.com"
}
resource "aws_cloudwatch_metric_alarm" "cpu_alarm" {
alarm_name = "high_cpu_usage"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "70"
alarm_actions = [
aws_sns_topic.alarm_topic.arn
]
}
AutoScaler IAM (iam-autoscaler.tf):
This will provision roles and policies for enabling the EKS Cluster Autoscaler are included, which will help in adjusting the number of worker nodes based on resource demands.
data "aws_iam_policy_document" "eks_cluster_autoscaler_assume_role_policy" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
effect = "Allow"
condition {
test = "StringEquals"
variable = "${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub"
values = ["system:serviceaccount:kube-system:cluster-autoscaler"]
}
principals {
identifiers = [aws_iam_openid_connect_provider.eks.arn]
type = "Federated"
}
}
}
resource "aws_iam_role" "eks_cluster_autoscaler" {
assume_role_policy = data.aws_iam_policy_document.eks_cluster_autoscaler_assume_role_policy.json
name = "eks-cluster-autoscaler"
}
resource "aws_iam_policy" "eks_cluster_autoscaler" {
name = "eks-cluster-autoscaler"
policy = jsonencode({
Statement = [{
Action = [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:DescribeTags",
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup",
"ec2:DescribeLaunchTemplateVersions"
]
Effect = "Allow"
Resource = "*"
}]
Version = "2012-10-17"
})
}
resource "aws_iam_role_policy_attachment" "eks_cluster_autoscaler_attach" {
role = aws_iam_role.eks_cluster_autoscaler.name
policy_arn = aws_iam_policy.eks_cluster_autoscaler.arn
}
output "eks_cluster_autoscaler_arn" {
value = aws_iam_role.eks_cluster_autoscaler.arn
}
Routing (security_groups.tf
)
This defines the security groups required for your infrastructure. Security groups act as virtual firewalls that control the inbound and outbound traffic to your resources.
touch security_groups.tf
# Provides a security group
resource "aws_security_group" "main_sg" {
name = "main_sg"
description = var.main_sg_description
vpc_id = aws_vpc.main.id
ingress {
description = "ssh access"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Kubernetes API access"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = var.tags_main_sg_eks
}
}
Variables (variables.tf
)
This contains the variable definitions used across your Terraform configurations. These variables provide default values and can be overridden as needed.
touch variables.tf
variable "region" {
type = string
default = "us-east-1"
}
#variable "profile" {
# type = string
# default = "tobi"
#}
variable "bucket_name" {
type = string
default = "regtech-logs"
}
variable "aws_access_key_id" {
type = string
default = ""
}
variable "aws_secret_access_key" {
type = string
default = ""
}
variable "tags_vpc" {
type = string
default = "main-vpc-eks"
}
variable "tags_public_rt" {
type = string
default = "public-route-table"
}
variable "tags_igw" {
type = string
default = "internet-gateway"
}
variable "tags_public_subnet_1" {
type = string
default = "public-subnet-1"
}
variable "tags_public_subnet_2" {
type = string
default = "public-subnet-2"
}
variable "tags_public_subnet_3" {
type = string
default = "public-subnet-3"
}
variable "tags_private_subnet_1" {
type = string
default = "private-subnet-1"
}
variable "tags_private_subnet_2" {
type = string
default = "private-subnet-2"
}
variable "tags_private_subnet_3" {
type = string
default = "private-subnet-3"
}
variable "tags_main_sg_eks" {
type = string
default = "main-sg-eks"
}
variable "instance_type" {
type = string
default = "t2.micro"
}
variable "cluster_name" {
type = string
default = "EKSCluster"
}
variable "node_group_name" {
type = string
default = "SlaveNode"
}
variable "vpc_cidr_block" {
type = string
default = "10.0.0.0/16"
}
variable "p_s_1_cidr_block" {
type = string
default = "10.0.1.0/24"
}
variable "az_a" {
type = string
default = "us-east-1a"
}
variable "p_s_2_cidr_block" {
type = string
default = "10.0.2.0/24"
}
variable "az_b" {
type = string
default = "us-east-1b"
}
variable "p_s_3_cidr_block" {
type = string
default = "10.0.3.0/24"
}
variable "az_c" {
type = string
default = "us-east-1c"
}
variable "private_s_1_cidr_block" {
type = string
default = "10.0.4.0/24"
}
variable "az_private_a" {
type = string
default = "us-east-1c"
}
variable "private_s_2_cidr_block" {
type = string
default = "10.0.5.0/24"
}
variable "az_private_b" {
type = string
default = "us-east-1c"
}
variable "private_s_3_cidr_block" {
type = string
default = "10.0.6.0/24"
}
variable "az_private_c" {
type = string
default = "us-east-1c"
}
variable "main_sg_description" {
type = string
default = "Allow TLS inbound traffic and all outbound traffic"
}
variable "eks_node_group_profile" {
type = string
default = "eks-node-group-instance-profile_log"
}
variable "eks_cluster_role_name" {
type = string
default = "eksclusterrole_log"
}
variable "eks_node_group_role_name" {
type = string
default = "eks-node-group-role_log"
}
variable "eks_node_group_volume_policy_name" {
type = string
default = "eks-node-group-volume-policy"
}
variable "eks_describe_cluster_policy_name" {
type = string
default = "eks-describe-cluster-policy_log"
}
variable "tags_nat" {
type = string
default = "nat-gateway_eip"
}
variable "tags_k8s-nat" {
type = string
default = "k8s-nat"
}
Provider (provider.tf
)
This is crucial in any Terraform project as it defines the provider configuration, which in this case is AWS.
touch
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = var.region
access_key = var.aws_access_key_id
secret_key = var.aws_secret_access_key
}
IAM OpenID (oidc.tf
)
This provides an IAM OpenID Connect provider.
touch oidc.tf
data "tls_certificate" "eks" {
url = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "eks" {
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint]
url = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer
}
Top comments (0)