diff --git a/README.md b/README.md index 8b5fec6..857095a 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,74 @@ $ make upgrade ## Deployment -This deployment command assumes you are locally authenticated to both gcloud and kubectl. Directions on how to do so are out of scope for this documentation. Please consult your team's local deployment tooling and instructions! +This deployment command assumes you are locally installed to gcloud and kubectl, in addition to all the other above installations + +Note that, during the deploy process, you will likely need to enable several google APIs. Do so when prompted, then run the deploy again. This will show up as messages like: + +> googleapi: Error 403: $API has not been used in project $PROECT before or it is disabled. Enable it by visiting... + +These instructions all assume you are starting from the top level directory, whenever a `$SHELL` command is given. + +### 0. Name your project + +Open `config.yml` and modify the "Project configuration" section + +### 1. Create a new project + +Create a new project via https://console.cloud.google.com/, then set its name in `config.yml` + +```yaml +# config.yml +project: dotted-hope-405813 +``` + +Then you should run the following commands once, replacing $PROJECT with your actual project ID. ```bash -source ./venv/bin/activate -invoke deploy # see tasks.py for source code +# $SHELL +$ gcloud config set project $PROJECT +$ gcloud auth application-default login --project $PROJECT +``` + +### 2. Create a terraform state bucket + +Create a terraform state bucket via https://console.cloud.google.com/, then set its name in `config.yml` + +```yaml +# config.yml +bucket: coilysiren-k8s-gpc-tfstate-3 +``` + +Then you must set its name manually in every `state.tf` file. Open every `state.tf` file in the repo. You will see a block like this: + +```hcl +terraform { + backend "gcs" { + bucket = "coilysiren-k8s-gpc-tfstate-3" + prefix = "terraform/state" + } +} +``` + +You should modify the `bucket = ...` line with your bucket name, same as in `config.yml`. + +Finally, import you import the bucket into terraform. + +```bash +# $SHELL +$ cd infrastructure/foundation/ +$ terraform init +$ terraform import google_storage_bucket.default coilysiren-k8s-gpc-tfstate-3 +``` + +Note that, when you deploy in the next step, you might have to modify the state bucket's region. The goal is to avoid replacing the state bucket. + +### 3. Deploy + +Run the deploy script + +```bash +# $SHELL +$ source ./venv/bin/activate +$ invoke deploy # see tasks.py for source code ``` diff --git a/config.yml b/config.yml index a924b65..7390293 100644 --- a/config.yml +++ b/config.yml @@ -1,3 +1,13 @@ -name: gke-test-2 -project: root-territory-384205 +# Project configuration +name: gke-test-3 +domain: gke-test-3.coilysiren.me +zone: coilysiren.me # domain - name = zone +email: coilysiren@gmail.com + +# Google Cloud Platform configuration +project: dotted-hope-405813 +statebucket: coilysiren-k8s-gpc-tfstate-3 region: us-central1 + +# https://github.com/cert-manager/cert-manager/releases +cert-manager-version: v1.13.2 diff --git a/infrastructure/application/.terraform.lock.hcl b/infrastructure/application/.terraform.lock.hcl index 731ba47..3761530 100644 --- a/infrastructure/application/.terraform.lock.hcl +++ b/infrastructure/application/.terraform.lock.hcl @@ -24,20 +24,20 @@ provider "registry.terraform.io/hashicorp/aws" { } provider "registry.terraform.io/hashicorp/google" { - version = "5.6.0" + version = "5.7.0" hashes = [ - "h1:4ALyLql3FFIU5xtZ1VJVoE9kQgI0PI8wO63sAKEdXT0=", - "zh:102b6a2672fade82114eb14ed46923fb1b74be2aaca3a50b4f35f7057a9a94b9", - "zh:1a56b63175068c67efbe7d130986ba2839a938f5ffc96a14fd450153174dbfa3", - "zh:1ba1c5e0c86e8aaa8037406390846e78c89b63faf9e527c7874641f35d436e1b", - "zh:3f7161b9288b47cbe89d2f9675f78d83b58ad5880c793b01f50a71ee2583844b", - "zh:66912d6e4180dac37185d17424b345a9d4e3c3c791d45e0737b35e32c9536b35", - "zh:6f06f56e9fac2e55b50e74ffac42d9522bb379394e51dca1eddd4c3b7a68545c", - "zh:8741861ebfa13bb1ed74ea7f4865388a0725ca3a781b6d873ce45e6a4630fe41", - "zh:ae89a9c538665fbc30bb83aa3b13acb18d8380e551ccf242e1c0ab4d626089ab", - "zh:c510f8321c7599aa601b1870fdc0c76cbad3054ed5cc70fe8e37a13a8046a71f", - "zh:cf143a53d5a25c6216d09a9c0b115bb473ffcebd5c4c62b2b2594b1ebc13e662", - "zh:de05b957e5dfdbaf92db47cd9b3ef46a0f8d94599eea6d472928f33058856add", + "h1:rCBKZinpkHWlZzXLE+iBcQNi5Q6KYqFRDIw0gX3OfLA=", + "zh:0c0cf15cc034d5f92cc1cd5ee4615012553a674b69ee1802e46c4b87f1c339aa", + "zh:28e64a798320866c4dc84c323b66eef94ec98043dba016cf01d6adbe2dc85de4", + "zh:3b6e6443a9000354f93682d847737d6e9f702a77c53a492a39b200134b3e8dfd", + "zh:3ed6af130702d9da8fc14f94b3b2c9a93917cda31d50d934dd6de0ca48044572", + "zh:784a0feae2a48aa9a63fe6feb86ad29e8d35647fa29eb42303b799f09ee92060", + "zh:828e0198d99b7f9e53994470d6b51012566660a560da9c8266d1eaf2b140635c", + "zh:8dcb7537d95ec14e75ca71cfce62323682ce0fb453902dc9f890b7c524a915d3", + "zh:a7e760dc5707603091a0c3de0d47d6f8e51d8cce523b5c90587b05f113c5e09c", + "zh:b5c79a5e5b9bcaf0158f5f704d31cf90fb93826085151f06dfc3ef48276ed17a", + "zh:c44a2726dcfbf7d538aa0d5abd2473108f625d1d82485a340e62dfc04043288f", + "zh:f4da66ba04847138949a6a178b8836182f7960e9d069bfe76f1203d9af99cd22", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } diff --git a/infrastructure/application/main.tf b/infrastructure/application/main.tf index 34d6dc4..20ab7e7 100644 --- a/infrastructure/application/main.tf +++ b/infrastructure/application/main.tf @@ -1,27 +1,88 @@ locals { - name = yamldecode(file("../../config.yml")).name + zone = yamldecode(file("../../config.yml")).zone + domain = yamldecode(file("../../config.yml")).domain + email = yamldecode(file("../../config.yml")).email } -# https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_config -data "google_client_config" "default" {} +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user +resource "aws_iam_user" "user" { + name = "certbot" +} + +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key +resource "aws_iam_access_key" "key" { + user = aws_iam_user.user.name +} + +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy +resource "aws_iam_policy" "policy" { + name = "certbot" + + # https://cert-manager.io/docs/configuration/acme/dns01/route53/ + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow", + Action = "route53:ChangeResourceRecordSets", + Resource = data.aws_route53_zone.zone.arn, + }, + { + Effect = "Allow" + Action = [ + "route53:Get*", + "route53:List*", + ] + Resource = "*" + }, + ] + }) +} + +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment +resource "aws_iam_user_policy_attachment" "test-attach" { + user = aws_iam_user.user.name + policy_arn = aws_iam_policy.policy.arn +} -data "kubernetes_service" "service" { +# https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret +resource "kubernetes_secret" "secret" { metadata { - name = "application" + name = "route53-credentials-secret" + } + + data = { + "access-key-id" = aws_iam_access_key.key.id + "secret-access-key" = aws_iam_access_key.key.secret + } +} + +# https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret +resource "kubernetes_secret" "cert" { + metadata { + name = "web-ssl" + } + type = "kubernetes.io/tls" + data = { + "tls.crt" = "" + "tls.key" = "" + } + lifecycle { + ignore_changes = [ + metadata, + data, + ] } } # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone data "aws_route53_zone" "zone" { - name = "coilysiren.me." + name = "${local.zone}." private_zone = false } -# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record -resource "aws_route53_record" "record" { - zone_id = data.aws_route53_zone.zone.zone_id - name = "${local.name}.coilysiren.me." - type = "A" - ttl = "300" - records = [data.kubernetes_service.service.status.0.load_balancer.0.ingress.0.ip] +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_address +resource "google_compute_address" "address" { + name = "application-ingress" + address_type = "EXTERNAL" } diff --git a/infrastructure/application/state.tf b/infrastructure/application/state.tf index d7d1798..d2d2b16 100644 --- a/infrastructure/application/state.tf +++ b/infrastructure/application/state.tf @@ -1,16 +1,23 @@ terraform { backend "gcs" { - bucket = "coilysiren-k8s-gpc-tfstate-0" + bucket = "coilysiren-k8s-gpc-tfstate-3" prefix = "terraform/state/application" } } +locals { + statebucket = yamldecode(file("../../config.yml")).statebucket +} + # https://registry.terraform.io/providers/hashicorp/google/latest/docs provider "google" { project = yamldecode(file("../../config.yml")).project region = yamldecode(file("../../config.yml")).region } +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_config +data "google_client_config" "default" {} + # https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs provider "kubernetes" { host = "https://${data.terraform_remote_state.foundation.outputs.endpoint}" @@ -25,13 +32,10 @@ provider "aws" { region = "us-east-1" } -# https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project -data "google_project" "default" {} - data "terraform_remote_state" "foundation" { backend = "gcs" config = { - bucket = "coilysiren-k8s-gpc-tfstate-0" + bucket = local.statebucket prefix = "terraform/state" } } diff --git a/infrastructure/deployment.yml b/infrastructure/deployment.yml new file mode 100644 index 0000000..287e849 --- /dev/null +++ b/infrastructure/deployment.yml @@ -0,0 +1,38 @@ +apiVersion: v1 +items: + - apiVersion: v1 + kind: Service + metadata: + annotations: + cloud.google.com/neg: '{"ingress": true}' + name: application + spec: + ports: + - name: http + port: 80 + targetPort: 8080 + selector: + app: application + type: ClusterIP + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: application + spec: + replicas: 1 + selector: + matchLabels: + app: application + template: + metadata: + labels: + app: application + spec: + containers: + - image: us-central1-docker.pkg.dev/dotted-hope-405813/repository/gke-test-3:certs-add8cea-kai + name: application + ports: + - containerPort: 8080 +kind: List +metadata: + resourceVersion: "" diff --git a/infrastructure/foundation/main.tf b/infrastructure/foundation/main.tf index 6e753cc..d1c4a71 100644 --- a/infrastructure/foundation/main.tf +++ b/infrastructure/foundation/main.tf @@ -2,9 +2,6 @@ locals { name = yamldecode(file("../../config.yml")).name } -# https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_config -data "google_client_config" "default" {} - # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_service_account resource "google_service_account" "gke" { account_id = local.name @@ -64,6 +61,28 @@ resource "google_project_iam_binding" "iamserviceAccountUser" { ] } +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam +# https://cloud.google.com/logging/docs/access-control +resource "google_project_iam_binding" "logWriter" { + project = data.google_client_config.default.project + role = "roles/logging.logWriter" + + members = [ + "serviceAccount:${data.google_project.default.number}@cloudservices.gserviceaccount.com", + ] +} + +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam +# https://cloud.google.com/monitoring/access-control +resource "google_project_iam_binding" "metricWriter" { + project = data.google_client_config.default.project + role = "roles/monitoring.metricWriter" + + members = [ + "serviceAccount:${data.google_project.default.number}@cloudservices.gserviceaccount.com", + ] +} + # https://registry.terraform.io/modules/terraform-google-modules/network/google/latest module "vpc" { source = "terraform-google-modules/network/google" @@ -111,19 +130,18 @@ module "vpc" { # https://registry.terraform.io/modules/terraform-google-modules/kubernetes-engine/google/latest module "gke" { - name = local.name - source = "terraform-google-modules/kubernetes-engine/google" - project_id = data.google_client_config.default.project - region = data.google_client_config.default.region - network = module.vpc.network_name - subnetwork = module.vpc.subnets_names[0] - zones = ["${data.google_client_config.default.region}-a"] # default is every zone, we only want one for $$$ reasons - remove_default_node_pool = true - deletion_protection = false - default_max_pods_per_node = 16 - initial_node_count = 1 - ip_range_pods = "pods-range" - ip_range_services = "services-range" + name = local.name + source = "terraform-google-modules/kubernetes-engine/google" + project_id = data.google_client_config.default.project + region = data.google_client_config.default.region + network = module.vpc.network_name + subnetwork = module.vpc.subnets_names[0] + zones = ["${data.google_client_config.default.region}-a"] # default is every zone, we only want one for $$$ reasons + remove_default_node_pool = true + deletion_protection = false + initial_node_count = 1 + ip_range_pods = "pods-range" + ip_range_services = "services-range" # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_node_pool # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#nested_node_config node_pools = [ diff --git a/infrastructure/foundation/state.tf b/infrastructure/foundation/state.tf index 6fe003d..c95284d 100644 --- a/infrastructure/foundation/state.tf +++ b/infrastructure/foundation/state.tf @@ -1,16 +1,23 @@ terraform { backend "gcs" { - bucket = "coilysiren-k8s-gpc-tfstate-0" + bucket = "coilysiren-k8s-gpc-tfstate-3" prefix = "terraform/state" } } +locals { + statebucket = yamldecode(file("../../config.yml")).statebucket +} + # https://registry.terraform.io/providers/hashicorp/google/latest/docs provider "google" { project = yamldecode(file("../../config.yml")).project region = yamldecode(file("../../config.yml")).region } +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_config +data "google_client_config" "default" {} + # https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project data "google_project" "default" {} @@ -20,7 +27,7 @@ data "google_project" "default" {} # # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket resource "google_storage_bucket" "default" { - name = "coilysiren-k8s-gpc-tfstate-0" + name = local.statebucket location = "US" force_destroy = true project = data.google_project.default.project_id diff --git a/infrastructure/ingress.yml b/infrastructure/ingress.yml new file mode 100644 index 0000000..13c2626 --- /dev/null +++ b/infrastructure/ingress.yml @@ -0,0 +1,50 @@ +apiVersion: v1 +items: + - apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + annotations: + cert-manager.io/issuer: letsencrypt-prod + kubernetes.io/ingress.allow-http: true + kubernetes.io/ingress.class: gce + kubernetes.io/ingress.global-static-ip-name: application-ingress + name: application-ingress + spec: + defaultBackend: + service: + name: application + port: + number: 80 + tls: + - hosts: + - gke-test-3.coilysiren.me + secretName: web-ssl + - apiVersion: cert-manager.io/v1 + kind: Issuer + metadata: + name: letsencrypt-prod + spec: + acme: + email: coilysiren@gmail.com + privateKeySecretRef: + name: letsencrypt-prod + server: https://acme-v02.api.letsencrypt.org/directory + solvers: + - http01: + ingress: + name: application-ingress + - dns01: + route53: + accessKeyIDSecretRef: + key: access-key-id + name: route53-credentials-secret + region: us-east-1 + secretAccessKeySecretRef: + key: secret-access-key + name: route53-credentials-secret + selector: + dnsZones: + - gke-test-3.coilysiren.me +kind: List +metadata: + resourceVersion: "" diff --git a/infrastructure/ingress/.terraform.lock.hcl b/infrastructure/ingress/.terraform.lock.hcl new file mode 100644 index 0000000..3761530 --- /dev/null +++ b/infrastructure/ingress/.terraform.lock.hcl @@ -0,0 +1,62 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.26.0" + hashes = [ + "h1:N6Iu1W6tvozB4RsClM9aHPuZhrKD6GCUAjlnl8THcLs=", + "zh:11a4062491e574c8e96b6bc7ced67b5e9338ccfa068223fc9042f9e1e7eda47a", + "zh:4331f85aeb22223ab656d04b48337a033f44f02f685c8def604c4f8f4687d10f", + "zh:915d6c996390736709f7ac7582cd41418463cfc07696218af6fea4a282df744a", + "zh:9306c306dbb2e1597037c54d20b1bd5f22a9cdcdb2b2b7bad657c8230bea2298", + "zh:93371860b9df369243219606711bfd3cfbd263db67838c06d5d5848cf47b6ede", + "zh:98338c17764a7b9322ddb6efd3af84e6890a4a0687f846eefdfb0fa03cec892d", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a28c9d77a5be25bac42d99418365757e4eb65a2c7c6788828263772cf2774869", + "zh:bd9c4648a090622d6b8c3c91dad513eec81e54db3dfe940ab6d155e5f37735e5", + "zh:bde63db136cccdeb282489e2ec2b3f9a7566edc9df27911a296352ab00832261", + "zh:ccd33f9490ce3f2d89efab995abf3b30e75579585f6a8a5b1f756246903d3518", + "zh:d73d1c461eb9d22833251f6533fc214cf014bc1d3165c5bfaa8ca29cd295ffb2", + "zh:db4ffb7eec5d0e1d0dbd0d65e1a3eaa6173a3337058105aec41fd0b2af5a2b46", + "zh:eb36b933419e9f6563330f3b7d53d4f1b09e62d27f7786d5dc6c4a2d0f6de182", + "zh:ec85ce1976e43f7d7fa10fa191c0a85e97326a3cb22387c0ed8b74d426ec94fd", + ] +} + +provider "registry.terraform.io/hashicorp/google" { + version = "5.7.0" + hashes = [ + "h1:rCBKZinpkHWlZzXLE+iBcQNi5Q6KYqFRDIw0gX3OfLA=", + "zh:0c0cf15cc034d5f92cc1cd5ee4615012553a674b69ee1802e46c4b87f1c339aa", + "zh:28e64a798320866c4dc84c323b66eef94ec98043dba016cf01d6adbe2dc85de4", + "zh:3b6e6443a9000354f93682d847737d6e9f702a77c53a492a39b200134b3e8dfd", + "zh:3ed6af130702d9da8fc14f94b3b2c9a93917cda31d50d934dd6de0ca48044572", + "zh:784a0feae2a48aa9a63fe6feb86ad29e8d35647fa29eb42303b799f09ee92060", + "zh:828e0198d99b7f9e53994470d6b51012566660a560da9c8266d1eaf2b140635c", + "zh:8dcb7537d95ec14e75ca71cfce62323682ce0fb453902dc9f890b7c524a915d3", + "zh:a7e760dc5707603091a0c3de0d47d6f8e51d8cce523b5c90587b05f113c5e09c", + "zh:b5c79a5e5b9bcaf0158f5f704d31cf90fb93826085151f06dfc3ef48276ed17a", + "zh:c44a2726dcfbf7d538aa0d5abd2473108f625d1d82485a340e62dfc04043288f", + "zh:f4da66ba04847138949a6a178b8836182f7960e9d069bfe76f1203d9af99cd22", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.23.0" + hashes = [ + "h1:cMs2scNCSgQhGamomGT5Ag4i8ms/mql1AR7NJc2hmbA=", + "zh:10488a12525ed674359585f83e3ee5e74818b5c98e033798351678b21b2f7d89", + "zh:1102ba5ca1a595f880e67102bbf999cc8b60203272a078a5b1e896d173f3f34b", + "zh:1347cf958ed3f3f80b3c7b3e23ddda3d6c6573a81847a8ee92b7df231c238bf6", + "zh:2cb18e9f5156bc1b1ee6bc580a709f7c2737d142722948f4a6c3c8efe757fa8d", + "zh:5506aa6f28dcca2a265ccf8e34478b5ec2cb43b867fe6d93b0158f01590fdadd", + "zh:6217a20686b631b1dcb448ee4bc795747ebc61b56fbe97a1ad51f375ebb0d996", + "zh:8accf916c00579c22806cb771e8909b349ffb7eb29d9c5468d0a3f3166c7a84a", + "zh:9379b0b54a0fa030b19c7b9356708ec8489e194c3b5e978df2d31368563308e5", + "zh:aa99c580890691036c2931841e88e7ee80d59ae52289c8c2c28ea0ac23e31520", + "zh:c57376d169875990ac68664d227fb69cd0037b92d0eba6921d757c3fd1879080", + "zh:e6068e3f94f6943b5586557b73f109debe19d1a75ca9273a681d22d1ce066579", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/infrastructure/ingress/main.tf b/infrastructure/ingress/main.tf new file mode 100644 index 0000000..903bae7 --- /dev/null +++ b/infrastructure/ingress/main.tf @@ -0,0 +1,26 @@ +locals { + zone = yamldecode(file("../../config.yml")).zone + domain = yamldecode(file("../../config.yml")).domain +} + +# https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/ingress_v1 +data "kubernetes_ingress_v1" "ingress" { + metadata { + name = "application-ingress" + } +} + +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone +data "aws_route53_zone" "zone" { + name = "${local.zone}." + private_zone = false +} + +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record +resource "aws_route53_record" "record" { + zone_id = data.aws_route53_zone.zone.zone_id + name = "${local.domain}." + type = "A" + ttl = "300" + records = [data.kubernetes_ingress_v1.ingress.status.0.load_balancer.0.ingress.0.ip] +} diff --git a/infrastructure/ingress/state.tf b/infrastructure/ingress/state.tf new file mode 100644 index 0000000..8a20fe4 --- /dev/null +++ b/infrastructure/ingress/state.tf @@ -0,0 +1,41 @@ +terraform { + backend "gcs" { + bucket = "coilysiren-k8s-gpc-tfstate-3" + prefix = "terraform/state/ingress" + } +} + +locals { + statebucket = yamldecode(file("../../config.yml")).statebucket +} + +# https://registry.terraform.io/providers/hashicorp/google/latest/docs +provider "google" { + project = yamldecode(file("../../config.yml")).project + region = yamldecode(file("../../config.yml")).region +} + +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_config +data "google_client_config" "default" {} + +# https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs +provider "kubernetes" { + host = "https://${data.terraform_remote_state.foundation.outputs.endpoint}" + token = data.google_client_config.default.access_token + cluster_ca_certificate = base64decode(data.terraform_remote_state.foundation.outputs.ca_certificate) +} + +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs +provider "aws" { + # AWS doesn't have the same regions as GCP, and also doesn't format then in the same way. + # That said, this isn't a huge issue because we are only using AWS for DNS. + region = "us-east-1" +} + +data "terraform_remote_state" "foundation" { + backend = "gcs" + config = { + bucket = local.statebucket + prefix = "terraform/state" + } +} diff --git a/infrastructure/kubeconfig.yml b/infrastructure/kubeconfig.yml deleted file mode 100644 index c5e40d8..0000000 --- a/infrastructure/kubeconfig.yml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: v1 -items: -- apiVersion: v1 - kind: Service - metadata: - annotations: - cloud.google.com/neg: '{"ingress":true}' - finalizers: - - service.kubernetes.io/load-balancer-cleanup - labels: - app: application - name: application - spec: - allocateLoadBalancerNodePorts: true - externalTrafficPolicy: Cluster - internalTrafficPolicy: Cluster - ports: - - port: 80 - targetPort: 8080 - selector: - app: application - type: LoadBalancer -- apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - app: application - name: application - spec: - replicas: 1 - selector: - matchLabels: - app: application - template: - metadata: - labels: - app: application - spec: - containers: - - image: us-central1-docker.pkg.dev/root-territory-384205/repository/gke-test-2:main-2b9bd06-kai - name: application - ports: - - containerPort: 8080 -kind: List -metadata: - resourceVersion: '' diff --git a/src/main/api.py b/src/main/api.py index 3ee219b..5a2285b 100644 --- a/src/main/api.py +++ b/src/main/api.py @@ -1,10 +1,20 @@ import flask -blueprint = flask.Blueprint("api", __name__, url_prefix="/api") +blueprint = flask.Blueprint("api", __name__, url_prefix="/") -@blueprint.route("/healthcheck") +@blueprint.route("") +def healthcheck_root(): + """used for debugging purposes""" + return flask.jsonify( + { + "status": "ok", + } + ) + + +@blueprint.route("api/healthcheck") def healthcheck(): """used for debugging purposes""" return flask.jsonify( diff --git a/src/test/test_api.py b/src/test/test_api.py index 3b91597..f0f447b 100644 --- a/src/test/test_api.py +++ b/src/test/test_api.py @@ -11,6 +11,12 @@ def test_true(): assert True +def test_healthcheck_root(): + response = client.get("/") + assert response.status_code == 200 + assert response.json == {"status": "ok"} + + def test_healthcheck(): response = client.get("/api/healthcheck") assert response.status_code == 200 diff --git a/tasks.py b/tasks.py index 3d20727..01a5fc4 100644 --- a/tasks.py +++ b/tasks.py @@ -1,13 +1,18 @@ #!/usr/bin/env python3 -import time import re import invoke import yaml +# https://stackoverflow.com/questions/25108581/python-yaml-dump-bad-indentation +class MyDumper(yaml.Dumper): + def increase_indent(self, flow=False, indentless=False): + return super(MyDumper, self).increase_indent(flow, False) + + class Context: """custom context class""" @@ -17,7 +22,6 @@ class Context: version: str docker_repo: str python_version: str - kubeconfig = "./infrastructure/kubeconfig.yml" def __init__(self, ctx) -> None: self.invoke = ctx @@ -57,13 +61,31 @@ def region(self) -> str: """get the region""" return self.config["region"] + @property + def email(self) -> str: + """get the email""" + return self.config["email"] + + @property + def domain(self) -> str: + """get the domain""" + return self.config["domain"] + @property def project(self) -> str: """get the project id""" return self.config["project"] - def get_kubeconfig(self) -> str: - with open(self.kubeconfig, "r", encoding="utf-8") as _file: + @property + def cert_manager_url(self) -> str: + """get the cert-manager url""" + return ( + "https://github.com/cert-manager/cert-manager/releases/download/" + f'{self.config["cert-manager-version"]}/cert-manager.yaml' + ) + + def get_kubeconfig(self, kubeconfig) -> str: + with open(kubeconfig, "r", encoding="utf-8") as _file: return yaml.safe_load(_file.read()) def update_image(self, kubeconfig: dict, image: str) -> dict: @@ -72,9 +94,23 @@ def update_image(self, kubeconfig: dict, image: str) -> dict: item["spec"]["template"]["spec"]["containers"][0]["image"] = image return kubeconfig - def write_kubeconfig(self, value: str) -> None: - with open(self.kubeconfig, "w", encoding="utf-8") as _file: - yaml.dump(value, _file) + def update_email(self, kubeconfig: dict, email: str) -> dict: + for item in kubeconfig["items"]: + if item["kind"] == "Issuer": + item["spec"]["acme"]["email"] = email + return kubeconfig + + def update_domain(self, kubeconfig: dict, domain: str) -> dict: + for item in kubeconfig["items"]: + if item["kind"] == "Ingress": + item["spec"]["tls"][0]["hosts"][0] = domain + if item["kind"] == "ClusterIssuer": + item["spec"]["acme"]["solvers"][0]["selector"]["dnsZones"][0] = domain + return kubeconfig + + def write_kubeconfig(self, kubeconfig, value: str) -> None: + with open(kubeconfig, "w", encoding="utf-8") as _file: + yaml.dump(value, _file, Dumper=MyDumper, default_flow_style=False) def _repo_name(self) -> str: """get the name of the repository""" @@ -124,9 +160,6 @@ def deploy(ctx: [invoke.Context, Context]): ctx.run("cd infrastructure/foundation && terraform init") ctx.run("cd infrastructure/foundation && terraform apply") - # set the project - ctx.run(f"gcloud config set project {ctx.project}") - # authenticate with gcloud for docker registry ctx.run( ctx.compress( @@ -149,14 +182,26 @@ def deploy(ctx: [invoke.Context, Context]): ctx.run(f"docker push {ctx.docker_repo}:{ctx.version}") # deploy to k8s cluster - kubeconfig = ctx.get_kubeconfig() + kubeconfig = ctx.get_kubeconfig("infrastructure/deployment.yml") kubeconfig = ctx.update_image(kubeconfig, f"{ctx.docker_repo}:{ctx.version}") - ctx.write_kubeconfig(kubeconfig) - ctx.run(f"kubectl apply -f {ctx.kubeconfig}") + ctx.write_kubeconfig("infrastructure/deployment.yml", kubeconfig) + ctx.run("kubectl apply -f infrastructure/deployment.yml") # deploy application infrastructure ctx.run("cd infrastructure/application && terraform init") ctx.run("cd infrastructure/application && terraform apply") + ctx.run(f"kubectl apply -f {ctx.cert_manager_url}") + + # deploy ingress + kubeconfig = ctx.get_kubeconfig("infrastructure/ingress.yml") + kubeconfig = ctx.update_email(kubeconfig, ctx.email) + kubeconfig = ctx.update_domain(kubeconfig, ctx.domain) + ctx.write_kubeconfig("infrastructure/ingress.yml", kubeconfig) + ctx.run("kubectl apply -f infrastructure/ingress.yml") + + # deploy the ingress DNS (eg. the domain name) + ctx.run("cd infrastructure/ingress && terraform init") + ctx.run("cd infrastructure/ingress && terraform apply") @invoke.task