diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 347f92fb..1a17ab91 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -91,60 +91,29 @@ jobs: - name: Build App run: make ${{ (env.DEPLOY_PROD == 'true' && 'prod') || 'stage' }} - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - if: ${{ env.DEPLOY_PROD == 'true' || env.DEPLOY_STAGE == 'true' }} - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - - - name: Login to Amazon ECR - id: login-ecr - if: ${{ env.DEPLOY_PROD == 'true' || env.DEPLOY_STAGE == 'true' }} - uses: aws-actions/amazon-ecr-login@v1 - - name: Make build context - if: ${{ env.DEPLOY_PROD == 'true' || env.DEPLOY_STAGE == 'true' }} run: | docker context create builders - name: Setup buildx uses: docker/setup-buildx-action@v2 - if: ${{ env.DEPLOY_PROD == 'true' || env.DEPLOY_STAGE == 'true' }} with: install: true endpoint: builders + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build docker image uses: docker/build-push-action@v3 - if: ${{ env.DEPLOY_PROD == 'true' || env.DEPLOY_STAGE == 'true' }} with: context: . file: docker/partial.Dockerfile tags: | - ${{ steps.login-ecr.outputs.registry }}/${{ (env.DEPLOY_PROD == 'true' && '7tv') || '7tv-stage' }}/website:latest - ${{ steps.login-ecr.outputs.registry }}/${{ (env.DEPLOY_PROD == 'true' && '7tv') || '7tv-stage' }}/website:${{ github.sha }} + ghcr.io/seventv/website:latest + ghcr.io/seventv/website:${{ github.sha }} push: true - - - name: Update deployment template - uses: danielr1996/envsubst-action@1.1.0 - if: ${{ env.DEPLOY_PROD == 'true' || env.DEPLOY_STAGE == 'true' }} - env: - IMAGE: ${{ steps.login-ecr.outputs.registry }}/${{ (env.DEPLOY_PROD == 'true' && '7tv') || '7tv-stage' }}/website:${{ github.sha }} - with: - input: k8s/${{ (env.DEPLOY_PROD == 'true' && 'production') || 'staging' }}.template.yaml - output: k8s/deploy.yaml - - - name: Setup Kubectl - uses: azure/setup-kubectl@v3.2 - - - name: Deploy to k8s - if: ${{ env.DEPLOY_PROD == 'true' || env.DEPLOY_STAGE == 'true' }} - env: - KUBE_CONFIG_DATA: ${{ (env.DEPLOY_PROD == 'true' && secrets.KUBECONFIG) || secrets.KUBECONFIG_STAGE }} - run: | - mkdir -p ~/.kube - (echo $KUBE_CONFIG_DATA | base64 -d) >> ~/.kube/config - - kubectl apply -f k8s/deploy.yaml diff --git a/k8s/.gitignore b/k8s/.gitignore deleted file mode 100644 index e0cc83b4..00000000 --- a/k8s/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!*.template.yaml diff --git a/k8s/production.template.yaml b/k8s/production.template.yaml deleted file mode 100644 index 772fcd0d..00000000 --- a/k8s/production.template.yaml +++ /dev/null @@ -1,103 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: website - namespace: app - labels: - app: website -spec: - strategy: - type: Recreate - selector: - matchLabels: - app: website - template: - metadata: - labels: - app: website - spec: - terminationGracePeriodSeconds: 30 - containers: - - name: website - image: ${IMAGE} - imagePullPolicy: Always - livenessProbe: - httpGet: - path: / - port: http - initialDelaySeconds: 1 - timeoutSeconds: 1 - periodSeconds: 3 - successThreshold: 1 - failureThreshold: 12 - readinessProbe: - httpGet: - path: / - port: http - initialDelaySeconds: 1 - timeoutSeconds: 1 - periodSeconds: 2 - successThreshold: 1 - failureThreshold: 12 - ports: - - name: http - containerPort: 3000 - envFrom: - - configMapRef: - name: website-config - resources: - requests: - memory: "500Mi" - cpu: "500m" - limits: - memory: "1Gi" - cpu: "1" ---- -apiVersion: v1 -kind: Service -metadata: - name: website - namespace: app -spec: - selector: - app: website - ports: - - port: 3000 - name: http - targetPort: http ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: website-config - namespace: app -data: - GQL_API_URL: http://api:3000/v3/gql - WEBSITE_URL: https://7tv.app - WEBSITE_BIND: 0.0.0.0:3000 ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: website - namespace: app - annotations: - kubernetes.io/ingress.class: nginx - external-dns.alpha.kubernetes.io/hostname: 7tv.app - external-dns.alpha.kubernetes.io/cloudflare-proxied: "true" -spec: - rules: - - host: 7tv.app - http: - paths: - - pathType: Prefix - path: / - backend: - service: - name: website - port: - name: http - tls: - - hosts: - - 7tv.app - secretName: 7tv-app-tls diff --git a/k8s/staging.template.yaml b/k8s/staging.template.yaml deleted file mode 100644 index 94657910..00000000 --- a/k8s/staging.template.yaml +++ /dev/null @@ -1,103 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: website - namespace: app - labels: - app: website -spec: - strategy: - type: Recreate - selector: - matchLabels: - app: website - template: - metadata: - labels: - app: website - spec: - terminationGracePeriodSeconds: 30 - containers: - - name: website - image: ${IMAGE} - imagePullPolicy: Always - livenessProbe: - httpGet: - path: / - port: http - initialDelaySeconds: 1 - timeoutSeconds: 1 - periodSeconds: 3 - successThreshold: 1 - failureThreshold: 12 - readinessProbe: - httpGet: - path: / - port: http - initialDelaySeconds: 1 - timeoutSeconds: 1 - periodSeconds: 2 - successThreshold: 1 - failureThreshold: 12 - ports: - - name: http - containerPort: 3000 - envFrom: - - configMapRef: - name: website-config - resources: - requests: - memory: "500Mi" - cpu: "500m" - limits: - memory: "1Gi" - cpu: "1" ---- -apiVersion: v1 -kind: Service -metadata: - name: website - namespace: app -spec: - selector: - app: website - ports: - - port: 3000 - name: http - targetPort: http ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: website-config - namespace: app -data: - GQL_API_URL: http://api:3000/v3/gql - WEBSITE_URL: https://7tv.dev - WEBSITE_BIND: 0.0.0.0:3000 ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: website - namespace: app - annotations: - kubernetes.io/ingress.class: nginx - external-dns.alpha.kubernetes.io/hostname: 7tv.dev - external-dns.alpha.kubernetes.io/cloudflare-proxied: "true" -spec: - rules: - - host: 7tv.dev - http: - paths: - - pathType: Prefix - path: / - backend: - service: - name: website - port: - name: http - tls: - - hosts: - - 7tv.dev - secretName: 7tv-dev-tls diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 00000000..8bdfa91f --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,2 @@ +.terraform.lock.hcl +.terraform diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 00000000..23c4a9dd --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,22 @@ +terraform { + backend "remote" { + hostname = "app.terraform.io" + organization = "7tv" + + workspaces { + prefix = "7tv-website-" + } + } +} + +locals { + workspace = trimprefix(terraform.workspace, "7tv-website-") +} + +module "eventapi" { + source = "./website" + docker_image = var.eventapi_docker_image + max_replicas = local.workspace == "prod" ? 10 : 1 + min_replicas = local.workspace == "prod" ? 5 : 1 + seventv_domain = var.seventv_domain +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 00000000..add35359 --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.18.1" + } + } +} + +locals { + kubeconfig = yamldecode(base64decode(data.terraform_remote_state.infra.outputs.kubeconfig)) +} + +provider "kubernetes" { + host = local.kubeconfig.clusters[0].cluster.server + cluster_ca_certificate = base64decode(local.kubeconfig.clusters[0].cluster.certificate-authority-data) + token = local.kubeconfig.users[0].user.token +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 00000000..55638cbd --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,19 @@ +variable "eventapi_docker_image" { + type = string + default = "ghcr.io/seventv/website:latest" +} + +data "terraform_remote_state" "infra" { + backend = "remote" + + config = { + organization = "7tv" + workspaces = { + name = "7tv-infra-${trimprefix(terraform.workspace, "7tv-website-")}" + } + } +} + +variable "seventv_domain" { + type = string +} diff --git a/terraform/website/main.tf b/terraform/website/main.tf new file mode 100644 index 00000000..2bb16fc3 --- /dev/null +++ b/terraform/website/main.tf @@ -0,0 +1,179 @@ +resource "kubernetes_namespace" "website" { + metadata { + name = var.namespace + } +} + +resource "kubernetes_deployment" "website" { + metadata { + name = "website" + labels = { + app = "website" + } + namespace = kubernetes_namespace.website.metadata[0].name + } + + timeouts { + create = "2m" + update = "2m" + delete = "2m" + } + + spec { + selector { + match_labels = { + app = "website" + } + } + template { + metadata { + labels = { + app = "website" + } + } + spec { + container { + name = "website" + image = var.docker_image + image_pull_policy = "Always" + port { + container_port = 3000 + name = "http" + } + readiness_probe { + initial_delay_seconds = 5 + period_seconds = 5 + tcp_socket { + port = "http" + } + } + liveness_probe { + initial_delay_seconds = 5 + period_seconds = 5 + tcp_socket { + port = "http" + } + } + startup_probe { + initial_delay_seconds = 5 + period_seconds = 5 + tcp_socket { + port = "http" + } + } + security_context { + allow_privilege_escalation = false + privileged = false + read_only_root_filesystem = true + run_as_non_root = true + run_as_user = 1000 + run_as_group = 1000 + capabilities { + drop = ["ALL"] + } + } + resources { + limits = { + "cpu" = "100m" + "memory" = "512Mi" + } + requests = { + "cpu" = "100m" + "memory" = "256Mi" + } + } + } + } + } + } +} + +resource "kubernetes_service" "website" { + metadata { + name = "website" + namespace = kubernetes_namespace.website.metadata[0].name + } + + spec { + selector = { + app = "website" + } + port { + name = "http" + port = 3000 + target_port = "http" + } + } +} + +resource "kubernetes_horizontal_pod_autoscaler_v2" "website" { + metadata { + name = "website" + namespace = kubernetes_namespace.website.metadata[0].name + } + + spec { + max_replicas = var.max_replicas + min_replicas = var.min_replicas + scale_target_ref { + api_version = "apps/v1" + kind = "Deployment" + name = kubernetes_deployment.website.metadata[0].name + } + metric { + type = "Resource" + resource { + name = "memory" + target { + type = "Utilization" + average_utilization = 75 + } + } + } + metric { + type = "Resource" + resource { + name = "cpu" + target { + type = "Utilization" + average_utilization = 75 + } + } + } + } +} + +resource "kubernetes_ingress_v1" "website" { + metadata { + name = "website" + namespace = kubernetes_namespace.website.metadata[0].name + annotations = { + "external-dns.alpha.kubernetes.io/hostname" = "${var.seventv_domain}" + "kubernetes.io/ingress.class" = "nginx" + "cert-manager.io/cluster-issuer" = "cloudflare" + } + } + spec { + rule { + host = var.seventv_domain + http { + path { + path = "/" + path_type = "Prefix" + backend { + service { + name = kubernetes_service.website.metadata[0].name + port { + name = "http" + } + } + } + } + } + } + tls { + hosts = [var.seventv_domain] + secret_name = "website-tls" + } + } +} diff --git a/terraform/website/providers.tf b/terraform/website/providers.tf new file mode 100644 index 00000000..c3d7f1cc --- /dev/null +++ b/terraform/website/providers.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.18.1" + } + random = { + source = "hashicorp/random" + version = "3.4.3" + } + } +} diff --git a/terraform/website/variables.tf b/terraform/website/variables.tf new file mode 100644 index 00000000..abc0d02c --- /dev/null +++ b/terraform/website/variables.tf @@ -0,0 +1,22 @@ +variable "docker_image" { + type = string +} + +variable "namespace" { + type = string + default = "website" +} + +variable "max_replicas" { + type = number + default = 10 +} + +variable "min_replicas" { + type = number + default = 5 +} + +variable "seventv_domain" { + type = string +}