This repository contains Terraform modules for deploying a web service into Kubernetes. The main module is sources/service-kubernetes
, but there are also other modules that can optionally be used alongside the main service module to assist with things like generating TLS certificates. All modules are described in the API section, below.
The creation of this module was driven by two main motivations: simplifying service deployment, and supporting blue-green deployments.
Simplifying service deployment
Many web services are relatively simple – they're built into a Docker image that needs to be deployed and have traffic routed to. Nevertheless, deploying such a service in a production Kubernetes cluster isn't quite as simple as this sounds. Multiple Kubernetes resources need to be created (a deployment
, a service
, an ingress
), with the correct configuration for each. This repository provides a module to simply configuring a service. It provides a simplified configuration API to solve the common use case of deploying a Docker image in production.
Supporting blue-green deployments
A commonly desired deployment strategy is a blue-green deployment. While this deployment strategy is not natively supported in Kubernetes, there are some services and tools that assist with blue-green deployments. Nevertheless, many of them introduce extra complexity and another moving part. The Terraform module in this repository supports blue-green deployments using only carefully-designed Terraform configuration. With less moving pieces there's less to learn and less to go wrong.
This module supports blue-green deployments in a no-compromise way – it is possible to use rolling updates instead of blue-green deployments, without creating any resources that aren't needed. See the API documentation below for more information.
This example deploys a Docker image to Kubernetes, configures DNS within Cloudflare, and generates TLS certificates for the service with Let's Encrypt. All three of these use cases are supported by modules within this repository.
terraform {
backend "s3" {
bucket = "my-user-terraform-state"
key = "production/my-web-service/terraform.tfstate"
region = "us-east-1"
acl = "private"
encrypt = true
dynamodb_table = "my-user-terraform-state-lock"
workspace_key_prefix = "production/my-web-service"
}
}
locals {
domain_name = "my-web-service.my-domain.com"
}
module "service" {
source = "git::ssh://git@github.com/edahlseng/terraform-module-service-kubernetes.git//sources/service-kubernetes?ref=vX.Y.Z"
active_environment = "blue"
# The variable below is needed due to limitations in Terraform.
# See the "Limitations and Configuration Oddities" section, below, for more information
current_stack_state_configuration = {
bucket = "my-user-terraform-state"
key = "production/my-web-service/terraform.tfstate"
region = "us-east-1"
acl = "private"
encrypt = true
dynamodb_table = "my-user-terraform-state-lock"
workspace_key_prefix = "production/my-web-service"
}
desired_count = 2
docker_image_active = "my-user/my-web-service:v1"
docker_image_passive = "my-user/my-web-service:v2"
ingresses = [
{
annotations = {},
rules = [
{
host = local.domain_name
path = "/"
tls_certificate_secret_name = module.tls_certificates.tls_certificate_secret_name[0]
}
]
}
]
max_surge = "100%"
max_unavailable = "25%"
port = 8080
resource_requests = {
cpu = "256m"
memory = "512Mi"
}
resource_limits = {
cpu = "128m"
memory = "256Mi"
}
service_name = "my-web-service"
service_state_output_name = "deployment_state"
}
output "deployment_state" {
value = module.service.deployment_state
}
# ------------------------------------------------------------------------------
# DNS
# ------------------------------------------------------------------------------
module "dns" {
source = "git::ssh://git@github.com/edahlseng/terraform-module-service-kubernetes.git//sources/service-dns-cloudflare?ref=vX.Y.Z"
foreach_workaround = [
{
hosted_zone_name = "my-domain.com"
include_active_environment = true
include_passive_environment = true
name = my-web-service
proxied = false
value = data.terraform_remote_state.cluster.outputs.cluster_domain_name
}
]
}
# ------------------------------------------------------------------------------
# TLS Certificates
# ------------------------------------------------------------------------------
resource "tls_private_key" "private_key" {
algorithm = "RSA"
}
resource "acme_registration" "reg" {
account_key_pem = tls_private_key.private_key.private_key_pem
email_address = "certificates@my-domain.com"
}
module "tls_certificates" {
source = "git@github.com:edahlseng/terraform-module-service-kubernetes.git//sources/tls-certificate-kubernetes?ref=vX.Y.Z"
acme_account_private_key_pem = tls_private_key.private_key.private_key_pem
acme_certificate_dns_provider = "cloudflare"
foreach_workaround = [
{
common_name = local.domain_name
subject_alternative_names = []
}
]
}
There are four modules in this repository: service-dns-cloudflare
, service-environment-kubernetes
, service-kubernetes
, and tls-certificate-kubernetes
.
This module creates DNS CNAME records in Cloudflare, creating passive CNAMES as well as active CNAMES, if specified. See the variables.tf file for the available parameters.
This module creates a single service "environment," representing either the blue environment or green environment in a blue-green deployment. This module generally should not be used externally. Instead, the service-kubernetes
module should be used, which consumes this module internally.
This module is the main module for this repository. It creates all of the Kubernetes resources needed to deploy a web service. See the variables.tf file for the available parameters.
This module is a convenience module for creating a TLS certificate through Let's Encrypt, as well as creating a Kubernetes secret containing the certificate. See the variables.tf file for the available parameters.
As noted above, one of the main motivations of this module is simplifying service deployment. Nevertheless, there are a lot of assumptions that this module cannot make about a particular team's deployment preferences. Therefore, when using this module across multiple services, there can be a lot of duplicated configuration.
It's strongly recommended that teams consuming this module create a "wrapper" module specific to their team. This wrapper module can use a similar set of variables as the service-kubernetes
module, specifying defaults that match the team's intended use case. This wrapper module can also handle common configuration patterns such as creating DNS records based on the specified ingress rules. A wrapper module reduces duplicated configuration across different services, and makes it much easier to spin up new services.
At the moment, there are a few limitations and awkward parts of the configuration API, required due to limitations within Terraform. Hopefully these can be cleaned up in the future as Terraform continues to evolve:
new
parameter- When deploying a brand new service for the first time, the
service-kubernetes
module requires itsnew
input variable to be set totrue
. This is easy to overlook for first-time users of this module, easy to forget for return users of this module, and the error message when this parameter is neglected does not make the problem immediately obvious. - This parameter will not be required once hashicorp/terraform Issue #22211 is resolved
- When deploying a brand new service for the first time, the
current_stack_state_configuration
parameter- Currently the
service-kubernetes
module requires acurrent_stack_state_configuration
parameter to be set. This requires duplication within the consuming configuration, and adds needless clutter. - This parameter will not be required once Terraform allows backend configuration to be referenced in other parts of the configuration. This enhancement has been mentioned in at least one issue comment, but it's unclear if an issue has been filed for this enhancement directly
- Currently the
- S3 backend required
- Currently the
service-kubernetes
module only works when the consuming stack uses an AWS S3 backend for storing the Terraform state - Other backends could be supported with extra work, though current limitations within Terraform's configuration language may make this more difficult. This limitation can be revisited if and when the community needs support for other backends, or once the Terraform configuration language makes this easier to support.
- Currently the