Date: 17 May 2023
In this article, I will demonstrate the process of provisioning various components of infrastructure, including Projects, Virtual Private Clouds (VPCs), Kubernetes clusters, Load Balancers, and DNS Records. Additionally, I will outline the steps to configure Kubernetes with ingress and cert-manager, all within a single pipeline.
For this purpose, I have chosen DigitalOcean as the cloud provider due to its cost-effectiveness in comparison to leading providers like AWS, GCP, and Azure. To facilitate the infrastructure provisioning, I will be utilizing Terraform, a powerful infrastructure as code tool. Unfortunately, I won't be able to incorporate Terragrunt into this setup, as I encountered difficulties in configuring it with DigitalOcean Bucket (Space).
The core of my project involves defining a live infrastructure along with several modules. The infrastructure definition comprises two distinct stages, which can be visualized through the provided diagram. You can find all of my code in my GitHub repository: https://github.com/andygolubev/terraform-digital-ocean
Initially, I attempted to handle both stages within a single script. However, during the implementation, I encountered a limitation with Terraform. Specifically, I discovered that Terraform's integration with the DigitalOcean provider did not allow for the creation and configuration of Kubernetes using the HashiCorp Kubernetes provider within the same script.
To address this challenge and ensure a smooth deployment process, I decided to split the infrastructure definition into two stages:
Stage 1: Infrastructure Provisioning (VPC, Kubernetes, PostgreSQL, Install Nginx and Cert manager) Stage 2: Kubernetes Configuration, DNS records setup
By separating the infrastructure provisioning and Kubernetes configuration into distinct stages, we can overcome the limitations imposed by the integration challenges mentioned earlier. This approach allows for granular control and flexibility when deploying and managing infrastructure and Kubernetes resources.
Furthermore, this division of stages enables better modularization and reusability, as each stage can be version-controlled, tested, and deployed independently. This not only simplifies the maintenance and troubleshooting process but also promotes scalability and agility when making changes or expanding the infrastructure in the future.
Overall, by navigating around the constraints and adopting a two-stage approach, we can effectively define and deploy our live infrastructure while integrating Kubernetes seamlessly into the process, ensuring a reliable and scalable environment for our applications.
This is my folders structure:
By separating the infrastructure provisioning and Kubernetes configuration into distinct stages, we can overcome the limitations imposed by the integration challenges mentioned earlier. This approach allows for granular control and flexibility when deploying and managing infrastructure and Kubernetes resources.
Furthermore, this division of stages enables better modularization and reusability, as each stage can be version-controlled, tested, and deployed independently. This not only simplifies the maintenance and troubleshooting process but also promotes scalability and agility when making changes or expanding the infrastructure in the future.
Overall, by navigating around the constraints and adopting a two-stage approach, we can effectively define and deploy our live infrastructure while integrating Kubernetes seamlessly into the process, ensuring a reliable and scalable environment for our applications.
This is my folders structure:
➜ terraform-digital-ocean git:(main) tree .
.
├── Infrastructure
│ └── digitalocean
│ ├── infrastructure-live
│ │ └── test-v1
│ │ ├── stage1
│ │ │ ├── main-stage1.tf
│ │ │ └── outputs.tf
│ │ └── stage2
│ │ ├── main-stage2.tf
│ │ └── outputs.tf
│ └── infrastructure-modules
│ ├── kubernetes-config
│ │ └── v1.0
│ │ ├── 0-versions.tf
│ │ ├── 1-save-kubeconfig.tf
│ │ ├── 2-cluster-issuer.tf
│ │ ├── 3-ingress-demo.tf
│ │ ├── 4-services-good-afternoon.tf
│ │ ├── 4-services-good-evening.tf
│ │ ├── 4-services-good-morning.tf
│ │ ├── 5-service-pagenotfound.tf
│ │ ├── 6-load-balancer.tf
│ │ ├── 7-records.tf
│ │ ├── 8-variables.tf
│ │ └── 9-outputs.tf
│ ├── kubernetes-provision
│ │ └── v1.0
│ │ ├── 0-versions.tf
│ │ ├── 1-kubernetes.tf
│ │ ├── 2-save-kubeconfig.tf
│ │ ├── 3-ingress-and-cert-manager.tf
│ │ ├── 4-registry-access.tf
│ │ ├── 5-variables.tf
│ │ └── 6-outputs.tf
│ ├── postgresql
│ │ └── v1.0
│ │ ├── 0-versions.tf
│ │ ├── 1-postgres.tf
│ │ ├── 2-variables.tf
│ │ └── 3-outputs.tf
│ └── vpc
│ └── v1.0
│ ├── 0-versions.tf
│ ├── 1-vpc.tf
│ ├── 2-variables.tf
│ └── 3-outputs.tf
├── LICENSE
└── README.md
Prior to executing my pipeline, I have created a private bucket in DigitalOcean for storing terraform states.
During this stage, I utilize my "main-stage1.tf" file to declare the necessary values for infrastructure provisioning. Additionally, I ensure proper management of dependencies between modules to guarantee a smooth and coherent deployment process.
...
resource "digitalocean_project" "this" {
name = "infra-demo-v1"
...
}
module "vpc" {
source = "../../../infrastructure-modules/vpc/v1.0"
vpc_name = "vpc-test"
...
}
module "kubernetes-provision" {
source = "../../../infrastructure-modules/kubernetes-provision/v1.0"
...
k8s_cluster_name = "demo-cluster-test-v1" #Edit
vpc_id = module.vpc.vpc_id
...
k8s_embedded_pool_size = "s-4vcpu-8gb"
k8s_embedded_pool_nodes_count = 1
# Type "true" if you want this pool of nodes
pool_1_enabled = true
k8s_pool_1_size = "s-4vcpu-8gb"
k8s_pool_1_nodes_count = 1
...
depends_on = [ module.vpc, digitalocean_project.this,]
}
module "postgresql" {
source = "../../../infrastructure-modules/postgresql/v1.0"
...
postgre_enabled = true
posgre_cluster_name = "postgresql-demo-test-v1"
...
depends_on = [ module.vpc, digitalocean_project.this,]
}
The complete content of the file: main-stage1.tf
The output of the Stage 1 is:
Outputs:
k8s_cluster_id = "ba4854df-c6de-4deb-8385-77014b491454"
k8s_cluster_name = "demo-cluster-test-v1"
k8s_cluster_urn = "do:kubernetes:ba4854df-c6de-4deb-8385-77014b491454"
In my pipeline, I utilize "k8s_cluster_name" output as an input for Stage 2. You can find details in the pipeline listing provided at the end of this article.
In Stage 2 of my pipeline, I use a combination of Kubernetes manifests, local command execution, and the creation of DigitalOcean resources to achieve the desired configuration and setup. You can see it in my "kubernetes-config" terraform module: main-stage2.tf
The "main-stage2.tf" file includes all the essential configurations for Stage 2.
The complete content of the file: main-stage2.tf
...
module "kubernetes-config" {
source = "../../../infrastructure-modules/kubernetes-config/v1.0"
digital_ocean_api_token_for_k8s_config = var.digital_ocean_api_token
k8s_config_cluster_name = var.k8s_cluster_name
domain = "kuber.work"
service1-subdomain = "service-1-test-morning"
service2-subdomain = "service-2-test-afternoon"
service3-subdomain = "service-3-test-evening"
lb-workaround-subdomain = "lb-workaround-test"
service1-service = "goodmorning"
service2-service = "goodafternoon"
service3-service = "goodevening"
cluster-issuer = "letsencrypt-prod" # letsencrypt-prod or letsencrypt-staging
ssl-redirect = "false" # To accommodate the requirement for the service to respond on HTTP, a temporary value is assigned for certificate issuing.
}
To illustrate and demonstrate the functionality of the system, I have incorporated three distinct services, each offering unique endpoints:
https://service-1-test-morning.kuber.work https://service-2-test-afternoon.kuber.work https://service-3-test-evening.kuber.work
For the automation I use two separate workflows:
Workflow for the infrastructure provision:
name: 1-Provision-infrastructure
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
PATH_STAGE_1: "./Infrastructure/digitalocean/infrastructure-live/test-v1/stage1/"
PATH_STAGE_2: "./Infrastructure/digitalocean/infrastructure-live/test-v1/stage2/"
jobs:
provision:
name: Provision the infrastructure in Digital Ocean and configure Kubernetes
runs-on: ubuntu-latest
steps:
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DO_API_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Provision the infrastructure
env:
DO_API_TOKEN: ${{secrets.DO_API_TOKEN }}
DO_BUCKET_ACCESS_KEY: ${{ secrets.DO_BUCKET_ACCESS_KEY }}
DO_BUCKET_SECRET_KEY: ${{ secrets.DO_BUCKET_SECRET_KEY }}
run: |
terraform -chdir=$PATH_STAGE_1 init -var="digital_ocean_api_token=$DO_API_TOKEN" -backend-config="access_key=$DO_BUCKET_ACCESS_KEY" -backend-config="secret_key=$DO_BUCKET_SECRET_KEY"
terraform -chdir=$PATH_STAGE_1 plan -var="digital_ocean_api_token=$DO_API_TOKEN"
terraform -chdir=$PATH_STAGE_1 apply -var="digital_ocean_api_token=$DO_API_TOKEN" --auto-approve
terraform -chdir=$PATH_STAGE_2 init -var="digital_ocean_api_token=$DO_API_TOKEN" -backend-config="access_key=$DO_BUCKET_ACCESS_KEY" -backend-config="secret_key=$DO_BUCKET_SECRET_KEY"
terraform -chdir=$PATH_STAGE_2 plan -var="digital_ocean_api_token=$DO_API_TOKEN" -var="k8s_cluster_name=$(cd $PATH_STAGE_1 && terraform output -raw k8s_cluster_name)"
terraform -chdir=$PATH_STAGE_2 apply -var="digital_ocean_api_token=$DO_API_TOKEN" --auto-approve -var="k8s_cluster_name=$(cd $PATH_STAGE_1 && terraform output -raw k8s_cluster_name)"
Here I use the output of the Stage 1 as the input for the Stage 2:
terraform -chdir=$PATH_STAGE_2 apply -var="digital_ocean_api_token=$DO_API_TOKEN" --auto-approve -var="k8s_cluster_name=$(cd $PATH_STAGE_1 && terraform output -raw k8s_cluster_name)"
so this command returns just the cluster name:
terraform output -raw k8s_cluster_name
Workflow for the infrastructure destruction:
name: 2-Destroy-infrastructure
on:
workflow_dispatch:
env:
PATH_STAGE_1: "./Infrastructure/digitalocean/infrastructure-live/test-v1/stage1/"
PATH_STAGE_2: "./Infrastructure/digitalocean/infrastructure-live/test-v1/stage2/"
jobs:
destroy:
name: Destroy the infrastructure in Digital Ocean
runs-on: ubuntu-latest
steps:
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DO_API_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Destroy the infrastructure
env:
DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }}
DO_BUCKET_ACCESS_KEY: ${{ secrets.DO_BUCKET_ACCESS_KEY }}
DO_BUCKET_SECRET_KEY: ${{ secrets.DO_BUCKET_SECRET_KEY }}
run: |
terraform -chdir=$PATH_STAGE_1 init -var="digital_ocean_api_token=$DO_API_TOKEN" -backend-config="access_key=$DO_BUCKET_ACCESS_KEY" -backend-config="secret_key=$DO_BUCKET_SECRET_KEY"
doctl auth init --access-token $DO_API_TOKEN
doctl kubernetes cluster kubeconfig save $(terraform -chdir=$PATH_STAGE_1 output -raw k8s_cluster_name) || true
terraform -chdir=$PATH_STAGE_2 init -var="digital_ocean_api_token=$DO_API_TOKEN" -backend-config="access_key=$DO_BUCKET_ACCESS_KEY" -backend-config="secret_key=$DO_BUCKET_SECRET_KEY" || true
terraform -chdir=$PATH_STAGE_2 apply -destroy -var="digital_ocean_api_token=$DO_API_TOKEN" --auto-approve -var="k8s_cluster_name=$(cd $PATH_STAGE_1 && terraform output -raw k8s_cluster_name)" || true
terraform -chdir=$PATH_STAGE_1 apply -destroy -var="digital_ocean_api_token=$DO_API_TOKEN" --auto-approve
In this stage, I begin by destroying the resources in Stage 2, followed by the destruction of all remaining resources.
To enable multiple runs of my pipelines, it is crucial to include the capability to destroy Stage 1 along with the main resources. To ignore any potential command failures before destruction of Stage 1, I use "|| true" expression.
terraform -chdir=$PATH_STAGE_2 apply -destroy --auto-approve || true
During my exploration, I encountered an issue with DigitalOcean regarding VPC and Project deletion. Despite not observing any associated resources in the console, DigitalOcean indicates that the VPC still possesses resources. Consequently, when attempting to remove the VPC using Terraform, it raises an error due to the inconsistency.
When I rerun the pipeline, this specific step executes without encountering any errors.
If you experienced this issue, please contact me or drop a comment.
Below, you will find a list of pipeline workflows:
In this article, I have demonstrated the process of provisioning various components of infrastructure using Terraform on DigitalOcean. By adopting a two-stage approach, I have overcome the limitations of the Terraform and the DigitalOcean providers.
You can find all of my code in my GitHub repository: https://github.com/andygolubev/terraform-digital-ocean
Feel free to connect with me on LinkedIn: https://www.linkedin.com/in/andy-golubev/