Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
9172be6
feat: add terraform files [skip ci]
May 11, 2025
94e1331
feat: include terraform checking in ci pipeline
May 11, 2025
ab11462
debug: test validity check
May 11, 2025
5109831
debug: test validity check
May 11, 2025
4ac2953
debug: print wd
May 11, 2025
b61750c
fix: set default run working directory
May 11, 2025
fb45fb7
debug: disable validity check
May 11, 2025
9614a0c
wip: add progress [skip ci]
May 11, 2025
35cf3f2
wip: add progress [skip ci]
May 11, 2025
69deed7
feat: add oidc authentication for user-managed identity
May 17, 2025
a0af4ed
feat: add oidc authentication for user-managed identity
May 17, 2025
17547ee
fix: whitelist paths
May 17, 2025
6abed82
fix: add azure cli login
May 17, 2025
a73e49d
fix: add permissions
May 17, 2025
aa3cd70
fix: remove secrets from backend.tf
May 17, 2025
5455fad
fix: remove secrets from backend.tf [skip ci]
May 17, 2025
a8a6949
fix: add manual trigger to infra pipeline
May 17, 2025
7c91681
fix: add manual trigger to infra pipeline
May 17, 2025
dab99e4
fix: syntax
May 17, 2025
e6c74ac
feat: add code to run terraform plan and apply
May 17, 2025
59d521d
debug: add pwd
May 17, 2025
f0cc484
fix: tf vars path
May 17, 2025
efdfb64
debug: try creating infrastructure
May 17, 2025
c7904e4
fix: update rg name
May 17, 2025
6449962
fix: import existing resource group
May 17, 2025
e01a6c5
fix: remove location property
May 17, 2025
707d964
fix: remove location property
May 17, 2025
236eb6f
feat: replace image name by real one [skip ci]
May 18, 2025
a69dbc7
fix: pipeline syntax
May 18, 2025
2cbb129
fix: add dockerhub username to tf plan
May 18, 2025
65229cf
debug: add tag to docker image
May 18, 2025
b3cbba8
debug: add authentication to dockerhub
May 18, 2025
3c64379
fix: variable name
May 18, 2025
e88ee53
fix: open another container port
May 18, 2025
2c959a1
feat: add infra destruction pipeline [skip ci]
May 18, 2025
52adec3
debug: run destruction pipeline
May 18, 2025
e37538e
fix: add variable file to destroy
May 18, 2025
80475db
fix: add variable file to destroy
May 18, 2025
d6ae1b2
fix: add variable file to destroy
May 18, 2025
091a518
fix: add variable file to destroy
May 18, 2025
75243ae
fix: add auto approve
May 18, 2025
a23494a
debug: remove debugging options
May 18, 2025
89103e7
fix: filename
May 18, 2025
02b04b0
docs: update docs
May 18, 2025
133e2b4
docs: update docs 2
May 18, 2025
2e08d56
docs: separate infra docs
May 18, 2025
a136240
fix: more detailed file paths
May 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions .github/workflows/ci-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,46 @@ on:
branches:
- main
- 'feature/**'
paths-ignore:
- 'images/**'
- '**/*.md'
paths:
- '.github/workflows/ci-pipeline.yaml'
- 'src/**/*'
- 'tests/**/*'
- 'terraform/**/*.tf'
- 'terraform/**/*.tfvars'
- 'Dockerfile'
- 'pyproject.toml'

env:
PYTHON_IMAGE: 'python:3.12-slim'
POETRY_VERSION: 2.1.2
DOCKER_BUILD_SUMMARY: false # Deactivate build summary generation

jobs:
check-infra-code:
name: Check Infrastructure Code
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./terraform
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Init Terraform CLI
uses: hashicorp/setup-terraform@v3

- name: Run Format Check
run: |
terraform fmt -check -diff

- name: Terraform Init
run: |
terraform init -input=false -backend=false

- name: Run Validation
run: |
terraform validate

test-and-check:
name: Test and Check Python Code
runs-on: ubuntu-latest
Expand Down
64 changes: 64 additions & 0 deletions .github/workflows/create-infra-pipeline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Create Infrastructure Pipeline

on:
push:
branches:
- main
- 'feature/**'
paths:
- '.github/workflows/create-infra-pipeline.yaml'
- 'terraform/**/*.tf'
- 'terraform/**/*.tfvars'

permissions:
id-token: write # Needed for Azure CLI Login

env:
TF_ENV_VARS_FILE_PATH: 'environments/dev.tfvars'
TF_PLAN_FILE_PATH: '/tmp/tf_plan'

jobs:
run-tf:
name: "Run Terraform Code"
runs-on: ubuntu-latest
environment: development # This would need to be environment-specific
defaults:
run:
working-directory: ./terraform
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.INFRA_ARM_CLIENT_ID }}
tenant-id: ${{ secrets.ARM_TENANT_ID }}
subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID }}

- name: Init Terraform CLI
uses: hashicorp/setup-terraform@v3

- name: Terraform Init
run: |
terraform init \
--backend-config=client_id=${{ secrets.INFRA_ARM_CLIENT_ID }} \
--backend-config=tenant_id=${{ secrets.ARM_TENANT_ID }}

- name: Terraform Plan
run: |
terraform plan \
-var-file=$TF_ENV_VARS_FILE_PATH \
-var 'dockerhub_username=${{ secrets.DOCKERHUB_USERNAME }}' \
-var 'dockerhub_password=${{ secrets.DOCKERHUB_TOKEN }}' \
-out=$TF_PLAN_FILE_PATH

- name: Terraform Apply
if: github.ref_name == 'main'
run: |
terraform apply \
-var-file=$TF_ENV_VARS_FILE_PATH \
-var 'dockerhub_username=${{ secrets.DOCKERHUB_USERNAME }}' \
-var 'dockerhub_password=${{ secrets.DOCKERHUB_TOKEN }}' \
-auto-approve \
$TF_PLAN_FILE_PATH
45 changes: 45 additions & 0 deletions .github/workflows/destroy-infra-pipeline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Destroy Infrastructure Pipeline

on:
workflow_dispatch:

permissions:
id-token: write # Needed for Azure CLI Login

env:
TF_ENV_VARS_FILE_PATH: 'environments/dev.tfvars'

jobs:
run-tf:
name: "Run Terraform Code"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./terraform
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.INFRA_ARM_CLIENT_ID }}
tenant-id: ${{ secrets.ARM_TENANT_ID }}
subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID }}

- name: Init Terraform CLI
uses: hashicorp/setup-terraform@v3

- name: Terraform Init
run: |
terraform init \
--backend-config=client_id=${{ secrets.INFRA_ARM_CLIENT_ID }} \
--backend-config=tenant_id=${{ secrets.ARM_TENANT_ID }}

- name: Terraform Destroy
run: |
terraform destroy \
-var-file=$TF_ENV_VARS_FILE_PATH \
-var 'dockerhub_username=${{ secrets.DOCKERHUB_USERNAME }}' \
-var 'dockerhub_password=${{ secrets.DOCKERHUB_TOKEN }}' \
-auto-approve
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,6 @@ cython_debug/

# PyPI configuration file
.pypirc

# Terraform
terraform/.terraform
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
A learning experience for setting up a CI/CD pipeline for Python

## ToDo
- Create Terraform scripts to deploy Docker image on Azure
- Create pipeline to setup infrastructure using Terraform scripts
- Create (separate?) pipeline to run continuous delivery on newly setup infrastructure
- Check remaining TODO comments
- Add project documentation

## Infrastructure

TODO: Infrastructure Sketch

For more detailed info see [Infrastructure README](terraform/README.md).
22 changes: 22 additions & 0 deletions terraform/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions terraform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Infrastructure

*In a real-world use case, one would seperate at least three environments of infrastructure: development, staging and production. This separation was out of the scope of this sample project. However, I showed how a possible separation could look like, e.g. with different .tfvar files.*

**Requirements**

For simplicity, I assumed that some resources are already present and did not automate the creation of them. I decided that this was out of scope of this sample project. Here are the (manual) steps you need to take in order to setup the project in Azure. This can be done either in the Azure Portal or via the Azure CLI:

- Create Accounts
- Create a DockerHub Account and an access token for it
- Create an Azure Account
- Create Resources
- Create a resource group (To contain all Azure resources associated with this repository)
- Create a storage account and container inside this resource group (To store Terraform state file)
- Create a user-managed identity
- Configure Resources
- Give the user-managed identity the following privileges:
- `Storage Blob Contributor` on the Storage Account Container (To read and write the Terraform state file)
- `Contributor` on the resource group (To add, change, and delete resources in it via Terraform)
- Create a federated credential for the main branch of your forked repository
- E.g. I created one with the following subject for the original repository: `repo:matrop/python-cicd:ref:refs/heads/main`
- Add GitHub Actions Secrets
- Add the client id of the newly created identity as GitHub Actions secret `INFRA_ARM_CLIENT_ID`
- Add the tenant id of your Azure account as GitHub Actions secret `ARM_TENANT_ID`
- Add the DockerHub Account name as GitHub Actions secret `DOCKERHUB_USERNAME`
- Add the DockerHub Account token as GitHub Actions secret `DOCKERHUB_TOKEN`

**Open ID Connect**

We use Open ID Connect (OIDC) in order to authenticate Terraform to Azure and provision resources. The usage of OIDC instead of client-secret-based authentication has several advantages:
- We do not need to store credentials in Azure or the CI/CD pipelines. They are requested automatically
- Credentials are short-lived and we do not need to rotate them
- Credentials are granular and enable a detailed access concept

One downside with this is the lack of wildcards in the subject identifier, e.g. for branch names. I read about it [here](https://learn.microsoft.com/en-us/answers/questions/2073829/azure-github-action-federated-identity-login-issue). In a real-world use case where many different tags or branches, e.g. feature branches, would make and apply infrastructure changes, this would be an issue.
9 changes: 9 additions & 0 deletions terraform/backend.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
terraform {
backend "azurerm" {
use_oidc = true
use_azuread_auth = true
storage_account_name = "samauriceatropsdev" # This would need to be environment-specific
container_name = "tfstate"
key = "terraform.tfstate"
}
}
3 changes: 3 additions & 0 deletions terraform/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
data "azurerm_resource_group" "rg" {
name = var.resource_group_name
}
11 changes: 11 additions & 0 deletions terraform/environments/dev.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource_group_name = "mauriceatrops-dev"
resource_group_location = "westeurope"

container_group_name = "maurice-sample-container-group-dev"
container_group_restart_policy = "Always"

container_name = "python-cicd-container"
container_image = "python-cicd:latest"
container_cpu_cores = 1
container_memory_in_gb = 1
container_port = 8080
26 changes: 26 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
resource "azurerm_container_group" "container" {
name = var.container_group_name
location = data.azurerm_resource_group.rg.location
resource_group_name = data.azurerm_resource_group.rg.name
ip_address_type = "Public"
os_type = "Linux"
restart_policy = var.container_group_restart_policy

container {
name = var.container_name
image = "${var.dockerhub_username}/${var.container_image}"
cpu = var.container_cpu_cores
memory = var.container_memory_in_gb

ports {
port = var.container_port
protocol = "TCP"
}
}

image_registry_credential {
server = "index.docker.io"
username = var.dockerhub_username
password = var.dockerhub_password
}
}
3 changes: 3 additions & 0 deletions terraform/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output "container_ipv4_address" {
value = azurerm_container_group.container.ip_address
}
13 changes: 13 additions & 0 deletions terraform/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
terraform {
required_version = ">=1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
}
}

provider "azurerm" {
features {}
}
60 changes: 60 additions & 0 deletions terraform/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
variable "resource_group_name" {
type = string
description = "Name of the resource group in which the project is deployed"
}

variable "resource_group_location" {
type = string
description = "Location of the resource group"
}

variable "container_group_name" {
type = string
description = "Name of the container group which runs the project image"
}

variable "container_group_restart_policy" {
type = string
description = "The behavior of Azure runtime if container has stopped"
validation {
condition = contains(["Always", "Never", "OnFailure"], var.container_group_restart_policy)
error_message = "The restart_policy must be one of the following: Always, Never, OnFailure."
}
}

variable "container_name" {
type = string
description = "Name of the container inside the container group which runs the project image"
}

variable "container_image" {
type = string
description = "Name of the image that should be run"
}

variable "container_cpu_cores" {
type = number
description = "Number of cpu cores the container will have available"
}

variable "container_memory_in_gb" {
type = number
description = "Storage size in GB the container will have available"
}

variable "container_port" {
type = number
description = "Port that the container exposes"
}

variable "dockerhub_username" {
type = string
sensitive = true
description = "Username (and repo name) for the image repository on Dockerhub"
}

variable "dockerhub_password" {
type = string
sensitive = true
description = "Pasword for the image repository on Dockerhub"
}