Skip to content
Merged

Iac #10

Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/api-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
- "shared/**"
- "go.work"
- "go.work.sum"
- ".github/workflows/api-image.yml"
- ".github/workflows/api-image.yaml"
pull_request:
branches:
- main
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tui-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
- "shared/**"
- "go.work"
- "go.work.sum"
- ".github/workflows/tui-image.yml"
- ".github/workflows/tui-image.yaml"
pull_request:
branches:
- main
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ profile.cov
# Helm
*.tgz
Charts/

# Terraform
terraform/.terraform
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
## Description
A Feature Flag service, self hosted or AWS cloud native.

## Currently it supports
- RBAC authorization natively
- TUI client so it can work on servers locally without needing a display env or compositor (and to cut cloud costs)
- You can only sign from the register endpoint, so you can't create an account from TUI to prevent dummy projects from existing (mostly, you still can use a regular client like **curl** or **postman** to create them like the demo)
- Every CRUD operation is done throught a **REST** endpoint, the real-time feature is done through a **Websocket** endpoint
- You can control **rollout percentage** (increase/decrease by only 5% at a time)
- Flag state is visible depending on time (unchanged for so long or not)
- Only one admin per project, to prevent collisions
- Deployable on **AWS Cloud** or locally on an **on-premise** server
- Supports **TLS** termination on the api layer (not end-to-end)
- All data are backed up regularly (configurable)

> **Note:** If you want to fork this project make sure to configure the secrets on your behalf as well

## Diagrams

### 1. Kubernetes Diagram
Expand All @@ -27,6 +41,7 @@ You can find the **eraser.io** diagram code here [AWS Cloud Diagram Text](assets
- Helm v3.19.0
- minikube v1.38.1 (for local dev)
- docker v29.6.1 (client + server)
- Terraform v1.15.6

## Used Packages

Expand Down Expand Up @@ -107,6 +122,39 @@ control
├── go.work.sum
├── LICENSE
├── README.md
├── terraform # Iac
│   ├── main.tf
│   ├── modules
│   │   ├── caching
│   │   │   ├── main.tf
│   │   │   ├── outputs.tf
│   │   │   └── variables.tf
│   │   ├── compute
│   │   │   ├── ecr.tf
│   │   │   ├── eks.tf
│   │   │   ├── outputs.tf
│   │   │   └── variables.tf
│   │   ├── data
│   │   │   ├── main.tf
│   │   │   ├── outputs.tf
│   │   │   └── variables.tf
│   │   ├── edge
│   │   │   ├── oidc.tf
│   │   │   ├── outputs.tf
│   │   │   ├── variables.tf
│   │   │   └── waf.tf
│   │   ├── security
│   │   │   ├── main.tf
│   │   │   ├── outputs.tf
│   │   │   └── variables.tf
│   │   └── vpc
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── outputs.tf
│   ├── providers.tf
│   ├── README.md
│   └── variables.tf
├── shared # Shared module between api and tui
│   ├── go.mod
│   └── types.go
Expand Down Expand Up @@ -463,3 +511,32 @@ The rest is the same
| TUI can't reach API | Verify `--network=host` is used when running TUI container |
| `invalid authorization format` | Ensure `Bearer $TOKEN` header is set correctly |
| Stale images after code changes | Re-run `go build` and `docker build` steps at top |

## AWS + Terraform
If you want to deploy it on the cloud (only AWS is supported for now), don't forget to configure your profile or use your credentials locally
```bash
nvim ~/.aws/credentials

# Which should contain
# [default]
# aws_access_key_id = <your-key-id>
# aws_secret_access_key = <your-secret>
```

Then you can
```bash
# If you changed anything in the tf files, make sure to format them (it's optional but recommended)
terraform fmt -recursive

# Validate to make sure there are no errors
terraform validate

# Then proceed with planning
terraform plan

# If you're ready to deploy, then
terraform apply
```

# Contributions
We currently accept contributions, but we don't mind if you fork this repo and build on top of it, so feel free to do so.
59 changes: 59 additions & 0 deletions terraform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.9 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | ~> 5.60 |

## Providers

No providers.

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_caching"></a> [caching](#module\_caching) | ./modules/caching | n/a |
| <a name="module_compute"></a> [compute](#module\_compute) | ./modules/compute | n/a |
| <a name="module_data"></a> [data](#module\_data) | ./modules/data | n/a |
| <a name="module_edge"></a> [edge](#module\_edge) | ./modules/edge | n/a |
| <a name="module_security"></a> [security](#module\_security) | ./modules/security | n/a |
| <a name="module_vpc"></a> [vpc](#module\_vpc) | ./modules/vpc | n/a |

## Resources

No resources.

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | n/a | `string` | `"us-east-1"` | no |
| <a name="input_azs"></a> [azs](#input\_azs) | n/a | `list` | <pre>[<br/> "us-east-1a",<br/> "us-east-1b"<br/>]</pre> | no |
| <a name="input_cache_node_type"></a> [cache\_node\_type](#input\_cache\_node\_type) | n/a | `string` | `"cache.t4g.micro"` | no |
| <a name="input_data_subnet_cidrs"></a> [data\_subnet\_cidrs](#input\_data\_subnet\_cidrs) | n/a | `list(string)` | n/a | yes |
| <a name="input_db_instance_class"></a> [db\_instance\_class](#input\_db\_instance\_class) | n/a | `string` | `"db.t4g.micro"` | no |
| <a name="input_db_password"></a> [db\_password](#input\_db\_password) | n/a | `any` | n/a | yes |
| <a name="input_db_username"></a> [db\_username](#input\_db\_username) | n/a | `string` | `"control"` | no |
| <a name="input_eks_cluster_version"></a> [eks\_cluster\_version](#input\_eks\_cluster\_version) | n/a | `string` | `"1.30"` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | n/a | `string` | `"prod"` | no |
| <a name="input_github_repo"></a> [github\_repo](#input\_github\_repo) | org/repo for the OIDC trust policy | `string` | `"ignorant05/control"` | no |
| <a name="input_node_desired_size"></a> [node\_desired\_size](#input\_node\_desired\_size) | n/a | `number` | `2` | no |
| <a name="input_node_instance_type"></a> [node\_instance\_type](#input\_node\_instance\_type) | n/a | `string` | `"t3.medium"` | no |
| <a name="input_private_subnet_cidrs"></a> [private\_subnet\_cidrs](#input\_private\_subnet\_cidrs) | n/a | `list(string)` | n/a | yes |
| <a name="input_project"></a> [project](#input\_project) | n/a | `string` | `"control"` | no |
| <a name="input_public_subnet_cidrs"></a> [public\_subnet\_cidrs](#input\_public\_subnet\_cidrs) | n/a | `list(string)` | n/a | yes |
| <a name="input_redis_auth_token"></a> [redis\_auth\_token](#input\_redis\_auth\_token) | n/a | `string` | n/a | yes |
| <a name="input_vpc_cidr"></a> [vpc\_cidr](#input\_vpc\_cidr) | n/a | `string` | `"10.0.0.0/16"` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_ecr_repository_url"></a> [ecr\_repository\_url](#output\_ecr\_repository\_url) | n/a |
| <a name="output_eks_cluster_name"></a> [eks\_cluster\_name](#output\_eks\_cluster\_name) | n/a |
| <a name="output_github_actions_role_arn"></a> [github\_actions\_role\_arn](#output\_github\_actions\_role\_arn) | n/a |
| <a name="output_rds_endpoint"></a> [rds\_endpoint](#output\_rds\_endpoint) | n/a |
| <a name="output_redis_endpoint"></a> [redis\_endpoint](#output\_redis\_endpoint) | n/a |
<!-- END_TF_DOCS -->
81 changes: 81 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
terraform {
required_version = ">= 1.9"

backend "s3" {
bucket = "control-tfstate"
key = "eks/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
encrypt = true
}

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}

module "vpc" {
source = "./modules/vpc"

aws_region = var.aws_region
project = var.project
vpc_cidr = var.vpc_cidr
azs = var.azs
public_subnet_cidrs = var.public_subnet_cidrs
private_subnet_cidrs = var.private_subnet_cidrs
data_subnet_cidrs = var.data_subnet_cidrs
}

module "compute" {
source = "./modules/compute"

project = var.project
environment = var.environment
eks_cluster_version = var.eks_cluster_version
vpc_id = module.vpc.vpc_id
private_subnets = module.vpc.private_subnets
node_instance_type = var.node_instance_type
node_desired_size = var.node_desired_size
}

module "security" {
source = "./modules/security"

project = var.project
vpc_id = module.vpc.vpc_id
eks_node_security_group_id = module.compute.node_security_group_id
}

module "data" {
source = "./modules/data"

project = var.project
environment = var.environment
data_subnets = module.vpc.data_subnets
security_group_id = module.security.rds_security_group_id
db_instance_class = var.db_instance_class
db_username = var.db_username
db_password = var.db_password
}

module "caching" {
source = "./modules/caching"

project = var.project
environment = var.environment
data_subnets = module.vpc.data_subnets
security_group_id = module.security.redis_security_group_id
cache_node_type = var.cache_node_type
redis_auth_token = var.redis_auth_token
}

module "edge" {
source = "./modules/edge"

project = var.project
github_repo = var.github_repo
eks_cluster_arn = module.compute.cluster_arn
}
18 changes: 18 additions & 0 deletions terraform/modules/caching/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "aws_elasticache_subnet_group" "this" {
name = "${var.project}-cache"
subnet_ids = var.data_subnets
}

resource "aws_elasticache_replication_group" "redis" {
replication_group_id = "${var.project}-${var.environment}"
description = "control feature-flag cache + pubsub"
engine = "redis"
engine_version = "7.1"
node_type = var.cache_node_type
num_cache_clusters = 1
subnet_group_name = aws_elasticache_subnet_group.this.name
security_group_ids = [var.security_group_id]
at_rest_encryption_enabled = true
transit_encryption_enabled = true
auth_token = var.redis_auth_token
}
3 changes: 3 additions & 0 deletions terraform/modules/caching/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output "primary_endpoint" {
value = aws_elasticache_replication_group.redis.primary_endpoint_address
}
24 changes: 24 additions & 0 deletions terraform/modules/caching/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
variable "project" {
type = string
}

variable "environment" {
type = string
}

variable "data_subnets" {
type = list(string)
}

variable "security_group_id" {
type = string
}

variable "cache_node_type" {
type = string
}

variable "redis_auth_token" {
type = string
sensitive = true
}
25 changes: 25 additions & 0 deletions terraform/modules/compute/ecr.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
resource "aws_ecr_repository" "control_api" {
name = "control-api"
image_tag_mutability = "IMMUTABLE"

image_scanning_configuration {
scan_on_push = true
}
}

resource "aws_ecr_lifecycle_policy" "control_api" {
repository = aws_ecr_repository.control_api.name
policy = jsonencode({
rules = [{
rulePriority = 1
description = "expire untagged images after 14 days"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 14
}
action = { type = "expire" }
}]
})
}
23 changes: 23 additions & 0 deletions terraform/modules/compute/eks.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.24"

cluster_name = "${var.project}-${var.environment}"
cluster_version = var.eks_cluster_version

vpc_id = var.vpc_id
subnet_ids = var.private_subnets

cluster_endpoint_public_access = true

eks_managed_node_groups = {
default = {
instance_types = [var.node_instance_type]
min_size = 1
max_size = var.node_desired_size + 2
desired_size = var.node_desired_size
}
}

enable_cluster_creator_admin_permissions = true
}
19 changes: 19 additions & 0 deletions terraform/modules/compute/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "cluster_name" {
value = module.eks.cluster_name
}

output "cluster_arn" {
value = module.eks.cluster_arn
}

output "cluster_endpoint" {
value = module.eks.cluster_endpoint
}

output "node_security_group_id" {
value = module.eks.node_security_group_id
}

output "ecr_repository_url" {
value = aws_ecr_repository.control_api.repository_url
}
Loading