├── CHANGELOG.md ├── src ├── gcp │ ├── scripts │ │ ├── copy-cnf.sh │ │ ├── add-user.sh │ │ └── my.cnf │ ├── versions.tf │ ├── storage.yaml │ ├── output.tf │ ├── k8s-secret.tf │ ├── k8s-db-pvc.tf │ ├── config.tf │ ├── k8s-config.tf │ ├── gke.tf │ ├── k8s-media.tf │ ├── variables.tf │ ├── nfs.tf │ ├── k8s-db.tf │ ├── k8s-waent.tf │ └── k8s-monitor.tf ├── azure │ ├── scripts │ │ ├── copy-cnf.sh │ │ ├── add-user.sh │ │ └── my.cnf │ ├── k8s-network.tf │ ├── k8s-secret.tf │ ├── k8s-storage.tf │ ├── network.tf │ ├── k8s-config.tf │ ├── main.tf │ ├── .terraform.lock.hcl │ ├── k8s-service.tf │ ├── k8s-compute.tf │ ├── variables.tf │ └── k8s-deployment.tf └── aws │ ├── wa_ent_db.yml │ ├── wa_ent_lambda.yml │ ├── wa_ent_net.yml │ └── wa_ent_monitoring.yml ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── .idea └── .gitignore ├── README.md └── CODE_OF_CONDUCT.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gcp/scripts/copy-cnf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | # 8 | 9 | # WhatsApp Business API GCP Template Version 1.0.1 10 | 11 | sleep 35s 12 | 13 | mkdir -p /etc/mysql/conf.d 14 | cp /var/mysql/init/my.cnf /etc/mysql/my.cnf 15 | -------------------------------------------------------------------------------- /src/azure/scripts/copy-cnf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | # 8 | 9 | # WhatsApp Business API Azure Template Version 1.0.0 10 | 11 | sleep 5s 12 | 13 | mkdir -p /etc/mysql/conf.d 14 | cp /var/mysql/init/my.cnf /etc/mysql/my.cnf 15 | -------------------------------------------------------------------------------- /src/gcp/versions.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | terraform { 11 | required_providers { 12 | google = { 13 | source = "hashicorp/google" 14 | version = ">= 3.44, < 5.0" 15 | } 16 | 17 | kubectl = { 18 | source = "gavinbunney/kubectl" 19 | version = ">= 1.7.0" 20 | } 21 | } 22 | 23 | required_version = ">= 0.14" 24 | } 25 | -------------------------------------------------------------------------------- /src/azure/scripts/add-user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | # 8 | 9 | # WhatsApp Business API Azure Template Version 1.0.0 10 | 11 | sleep 5s 12 | 13 | mysql -uroot -p"$WA_DB_PASSWORD" -e "CREATE USER IF NOT EXISTS $WA_DB_USERNAME@'%' IDENTIFIED BY '$WA_DB_PASSWORD';" 14 | mysql -uroot -p"$WA_DB_PASSWORD" -e "GRANT ALL PRIVILEGES ON *.* TO '$WA_DB_USERNAME'@'%' IDENTIFIED BY '$WA_DB_PASSWORD';" 15 | mysql -uroot -p"$WA_DB_PASSWORD" -e "FLUSH PRIVILEGES;" 16 | -------------------------------------------------------------------------------- /src/gcp/scripts/add-user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | # 8 | 9 | # WhatsApp Business API GCP Template Version 1.0.0 10 | sleep 35s 11 | 12 | mkdir -p /etc/mysql/conf.d 13 | cp /var/mysql/init/my.cnf /etc/mysql/my.cnf 14 | 15 | sleep 1 16 | mysql -uroot -p"$WA_DB_PASSWORD" -e "CREATE USER $WA_DB_USERNAME@'%' IDENTIFIED BY '$WA_DB_PASSWORD';" 17 | mysql -uroot -p"$WA_DB_PASSWORD" -e "GRANT ALL PRIVILEGES ON *.* TO '$WA_DB_USERNAME'@'%' IDENTIFIED BY '$WA_DB_PASSWORD';" 18 | mysql -uroot -p"$WA_DB_PASSWORD" -e "FLUSH PRIVILEGES;" 19 | 20 | sleep 3s 21 | -------------------------------------------------------------------------------- /src/gcp/storage.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | apiVersion: v1 11 | kind: PersistentVolume 12 | metadata: 13 | name: nfs-pv 14 | spec: 15 | capacity: 16 | storage: 200Gi 17 | accessModes: 18 | - ReadWriteMany 19 | mountOptions: 20 | - hard 21 | - nconnect=8 22 | - nfsvers=4.1 23 | - rsize=1048576 24 | - wsize=1048576 25 | - actimeo=120 26 | - timeo=600 27 | nfs: 28 | server: nfs-server.default.svc.cluster.local 29 | path: "/" 30 | 31 | --- 32 | kind: PersistentVolumeClaim 33 | apiVersion: v1 34 | metadata: 35 | name: nfs-pvc 36 | spec: 37 | accessModes: 38 | - ReadWriteMany 39 | storageClassName: "" 40 | resources: 41 | requests: 42 | storage: 200Gi 43 | -------------------------------------------------------------------------------- /src/gcp/output.tf: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | # This source code is licensed under the MIT license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | # 8 | 9 | # WhatsApp Business API GCP Template Version 1.0.0 10 | 11 | output "throughput" { 12 | value = var.throughput 13 | } 14 | 15 | output "app-machine-type" { 16 | value = var.map_coreapp_class[var.message_type][var.throughput] 17 | } 18 | 19 | output "db-machine-type" { 20 | value = var.map_db_class[var.throughput] 21 | } 22 | 23 | output "number_of_shards" { 24 | value = var.map_shards_count[var.throughput] 25 | } 26 | 27 | output "webapp_lb_ip" { 28 | value = kubernetes_service.webapp[*].status.0.load_balancer.0.ingress.0.ip 29 | } 30 | 31 | output "monitor_lb_ip" { 32 | value = kubernetes_service.monitor[*].status.0.load_balancer.0.ingress.0.ip 33 | } 34 | 35 | output "cluster_authentication" { 36 | value = "gcloud container clusters get-credentials ${google_container_cluster.waApi.name} --zone ${var.zone}" 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | debug 2 | .DS_Store 3 | .vscode 4 | *.bk 5 | 6 | # Local .terraform directories 7 | **/.terraform/* 8 | 9 | #.tfplan files 10 | *.tfplan 11 | 12 | # .tfstate files 13 | *.tfstate 14 | *.tfstate.* 15 | 16 | # Crash log files 17 | crash.log 18 | crash.*.log 19 | 20 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 21 | # password, private keys, and other secrets. These should not be part of version 22 | # control as they are data points which are potentially sensitive and subject 23 | # to change depending on the environment. 24 | *.tfvars 25 | *.tfvars.json 26 | 27 | # Ignore override files as they are usually used to override resources locally and so 28 | # are not checked in 29 | override.tf 30 | override.tf.json 31 | *_override.tf 32 | *_override.tf.json 33 | 34 | # Include override files you do wish to add to version control using negated pattern 35 | # !example_override.tf 36 | 37 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 38 | # example: *tfplan* 39 | 40 | # Ignore CLI configuration files 41 | .terraformrc 42 | terraform.rc 43 | .terraform.lock.hcl 44 | .terraform 45 | terraform.tfstate* 46 | .variable* 47 | logs 48 | -------------------------------------------------------------------------------- /src/azure/k8s-network.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | resource "azurerm_public_ip" "waApi" { 10 | name = "${module.naming.public_ip.name}-waApi" 11 | resource_group_name = "${module.naming.resource_group.name_unique}-aks-node" 12 | location = azurerm_resource_group.waApi.location 13 | domain_name_label = "${var.name-prefix}-${var.owner}-web-lb" 14 | allocation_method = "Static" 15 | sku = "Standard" 16 | 17 | tags = { 18 | owner = var.owner 19 | } 20 | 21 | depends_on = [azurerm_kubernetes_cluster.waApi] 22 | } 23 | 24 | resource "azurerm_public_ip" "waMon" { 25 | name = "${module.naming.public_ip.name}-waMon" 26 | resource_group_name = "${module.naming.resource_group.name_unique}-aks-node" 27 | location = azurerm_resource_group.waApi.location 28 | domain_name_label = "${var.name-prefix}-${var.owner}-mon-lb" 29 | allocation_method = "Static" 30 | sku = "Standard" 31 | 32 | tags = { 33 | owner = var.owner 34 | } 35 | 36 | depends_on = [azurerm_kubernetes_cluster.waApi] 37 | } 38 | -------------------------------------------------------------------------------- /src/gcp/k8s-secret.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | locals { 11 | data_source_name = "${var.dbusername}:${var.dbpassword}@(${kubernetes_service.db.metadata.0.name}:3306)/" #vm server 12 | wa_db_username = var.dbusername #vm server && flexible server 13 | } 14 | resource "kubernetes_secret" "env" { 15 | metadata { 16 | name = "secret-env" 17 | } 18 | 19 | data = { 20 | WA_DB_USERNAME = local.wa_db_username 21 | WA_DB_PASSWORD = var.dbpassword 22 | } 23 | } 24 | 25 | resource "kubernetes_secret" "db" { 26 | metadata { 27 | name = "secret-db" 28 | } 29 | 30 | data = { 31 | DATA_SOURCE_NAME = local.data_source_name 32 | } 33 | } 34 | 35 | resource "kubernetes_secret" "mon-prom" { 36 | metadata { 37 | name = "secret-mon-prom" 38 | } 39 | 40 | data = { 41 | WA_WEB_PASSWORD = var.wabiz-web-password 42 | } 43 | } 44 | 45 | resource "kubernetes_secret" "mon-graf" { 46 | metadata { 47 | name = "secret-mon-graf" 48 | } 49 | 50 | data = { 51 | GF_SECURITY_ADMIN_PASSWORD = var.mon-web-password 52 | GF_SMTP_PASSWORD = var.mon-smtp-password 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/azure/scripts/my.cnf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.0 9 | 10 | [mysqld] 11 | max_connections=3000 12 | # please adjust below parameters based on the spec of server 13 | # current value is based on Standard_E16as_v4 (16vCPU,128GB) 14 | innodb_buffer_pool_size=103079215104 15 | innodb_buffer_pool_instances=32 16 | query_cache_size = 8589934592 17 | 18 | # Standard_E8as_v4 (8vCPU,64GB) 19 | # innodb_buffer_pool_size=51539607552 20 | # innodb_buffer_pool_instances=16 21 | # query_cache_size = 4294967296 22 | 23 | # Standard_E4as_v4 (4vCPU,32GB) 24 | # innodb_buffer_pool_size=25769803776 25 | # innodb_buffer_pool_instances=16 26 | # query_cache_size = 2147483648 27 | 28 | # Standard_E2as_v4 (2vCPU,16GB) 29 | # innodb_buffer_pool_size=12884901888 30 | # innodb_buffer_pool_instances=8 31 | # query_cache_size = 1073741824 32 | 33 | query_cache_type=1 34 | log_error=/var/lib/mysql/db-0.err 35 | log_error_verbosity=1 36 | #enable below could reduce performance 37 | # slow_query_log=1 38 | # long_query_time=0.05 39 | # 40 | # * IMPORTANT: Additional settings that can override those from this file! 41 | # The files must end with '.cnf', otherwise they'll be ignored. 42 | # 43 | !includedir /etc/mysql/conf.d/ 44 | -------------------------------------------------------------------------------- /src/gcp/scripts/my.cnf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | # WhatsApp Business API GCP Template Version 1.0.0 8 | 9 | [mysqld] 10 | max_connections=3000 11 | #current value is based on throughput [250, 300], map_db_class n2-highmem-32 (32vCPU, 256G) 12 | innodb_buffer_pool_size=206158430208 13 | innodb_buffer_pool_instances=32 14 | query_cache_size = 17179869184 15 | 16 | #current value is based on throughput [200], map_db_class n2-highmem-16 (16CPU, 128GB) 17 | #innodb_buffer_pool_size=103079215104 18 | #innodb_buffer_pool_instances=32 19 | #query_cache_size = 8589934592 20 | 21 | #current value is based on throughput [20,40,80,120,160], map_db_class n2-highmem-8 (8vCPU, 64G) 22 | #innodb_buffer_pool_size=51539607552 23 | #innodb_buffer_pool_instances=16 24 | #query_cache_size = 4294967296 25 | 26 | # current value is based on throughput [10], map_db_class n2-highmem-4(4vCPU,32GB) 27 | # innodb_buffer_pool_size=25769803776 28 | # innodb_buffer_pool_instances=16 29 | # query_cache_size = 2147483648 30 | 31 | 32 | query_cache_type=1 33 | log_error=/var/lib/mysql/db-0.err 34 | log_error_verbosity=1 35 | 36 | # * IMPORTANT: Additional settings that can override those from this file! 37 | # The files must end with '.cnf', otherwise they'll be ignored. 38 | # 39 | !includedir /etc/mysql/conf.d/ 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to WhatsApp Business API Setup Scripts 2 | 3 | We want to make contributing to this project as easy and transparent as 4 | possible. 5 | 6 | ## Our Development Process 7 | 8 | We expect to ship changes to existing setup scripts and add new setup scripts on an ongoing basis. 9 | 10 | ## Pull Requests 11 | 12 | We actively welcome your pull requests. 13 | 14 | 1. Fork the repo and create your branch from `main`. 15 | 2. Make sure your changes lint and work with all past and present versions of WhatsApp Business API. 16 | 3. If you haven't already, complete the Contributor License Agreement ("CLA"). 17 | 18 | ## Contributor License Agreement ("CLA") 19 | 20 | In order to accept your pull request, we need you to submit a CLA. You only need 21 | to do this once to work on any of Facebook's open source projects. 22 | 23 | Complete your CLA here: 24 | 25 | ## Issues 26 | 27 | We use GitHub issues to track public bugs. Please ensure your description is 28 | clear and has sufficient instructions to be able to reproduce the issue. 29 | 30 | For issues on your integration with WhatsApp Business API, please use our 31 | support channel at . 32 | 33 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 34 | disclosure of security bugs. In those cases, please go through the process 35 | outlined on that page and do not file a public issue. 36 | 37 | ## License 38 | 39 | By contributing to WhatsApp Business API Setup Scripts, you agree that your contributions will be licensed 40 | under the LICENSE file in the root directory of this source tree. 41 | -------------------------------------------------------------------------------- /src/azure/k8s-secret.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | 10 | locals { 11 | # data_source_name = "${var.dbusername}:${var.dbpassword}@(${azurerm_mysql_flexible_server.waApi.fqdn}:3306)/" #flexible server 12 | # data_source_name = "${var.dbusername}@${azurerm_mysql_server.waApi.fqdn}:${var.dbpassword}@(${azurerm_mysql_server.waApi.fqdn}:3306)/?allowNativePasswords=true" #single server 13 | data_source_name = "${var.dbusername}:${var.dbpassword}@(${kubernetes_service.db.metadata.0.name}:3306)/" #vm server 14 | # wa_db_username = "${var.dbusername}@${azurerm_mysql_server.waApi.fqdn}" #single server 15 | wa_db_username = var.dbusername #vm server && flexible server 16 | } 17 | resource "kubernetes_secret" "env" { 18 | metadata { 19 | name = "secret-env" 20 | # namespace = kubernetes_namespace.deployment.metadata.0.name 21 | } 22 | 23 | data = { 24 | WA_DB_USERNAME = local.wa_db_username 25 | WA_DB_PASSWORD = var.dbpassword 26 | } 27 | } 28 | 29 | resource "kubernetes_secret" "db" { 30 | metadata { 31 | name = "secret-db" 32 | # namespace = kubernetes_namespace.deployment.metadata.0.name 33 | } 34 | 35 | data = { 36 | DATA_SOURCE_NAME = local.data_source_name 37 | } 38 | } 39 | 40 | resource "kubernetes_secret" "mon-prom" { 41 | metadata { 42 | name = "secret-mon-prom" 43 | # namespace = kubernetes_namespace.deployment.metadata.0.name 44 | } 45 | 46 | data = { 47 | WA_WEB_PASSWORD = var.wabiz-web-password 48 | } 49 | } 50 | 51 | resource "kubernetes_secret" "mon-graf" { 52 | metadata { 53 | name = "secret-mon-graf" 54 | # namespace = kubernetes_namespace.deployment.metadata.0.name 55 | } 56 | 57 | data = { 58 | GF_SECURITY_ADMIN_PASSWORD = var.mon-web-password 59 | GF_SMTP_PASSWORD = var.mon-smtp-password 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/azure/k8s-storage.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | 10 | resource "kubernetes_persistent_volume_claim" "media-share" { 11 | metadata { 12 | name = "media-share" 13 | } 14 | spec { 15 | access_modes = ["ReadWriteMany"] 16 | resources { 17 | requests = { 18 | storage = "100Gi" 19 | } 20 | } 21 | 22 | storage_class_name = kubernetes_storage_class.media-share.metadata.0.name 23 | } 24 | 25 | depends_on = [azurerm_role_assignment.waApi] 26 | } 27 | 28 | resource "kubernetes_storage_class" "media-share" { 29 | metadata { 30 | name = "media-share" 31 | } 32 | # storage_provisioner = "kubernetes.io/azure-file" 33 | storage_provisioner = "file.csi.azure.com" 34 | reclaim_policy = "Retain" 35 | parameters = { 36 | protocol = "nfs" 37 | skuName = "Premium_LRS" 38 | location = var.location 39 | } 40 | #mount_options = ["file_mode=0666", "dir_mode=0777", "mfsymlinks", "uid=0", "gid=0", "cache=strict"] ///smb option only 41 | mount_options = ["nconnect=8", "nfsvers=4.1", "rsize=1048576", "wsize=1048576", "hard", "actimeo=120", "timeo=600", "retrans=2"] ///nfs option only 42 | } 43 | 44 | 45 | resource "kubernetes_persistent_volume_claim" "db" { 46 | metadata { 47 | name = "db" 48 | labels = {} 49 | annotations = {} 50 | } 51 | spec { 52 | access_modes = ["ReadWriteOnce"] 53 | resources { 54 | requests = { 55 | storage = "64Gi" 56 | } 57 | } 58 | 59 | storage_class_name = kubernetes_storage_class.db.metadata.0.name 60 | } 61 | } 62 | 63 | resource "kubernetes_storage_class" "db" { 64 | metadata { 65 | name = "db" 66 | } 67 | 68 | storage_provisioner = "disk.csi.azure.com" 69 | reclaim_policy = "Retain" 70 | volume_binding_mode = "Immediate" 71 | allow_volume_expansion = true 72 | 73 | parameters = { 74 | skuName = "UltraSSD_LRS" 75 | kind = "managed" 76 | cachingMode = "None" 77 | diskIopsReadWrite : var.map_db_iops[var.throughput] 78 | diskMbpsReadWrite : var.map_db_throughput[var.throughput] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/gcp/k8s-db-pvc.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | resource "kubernetes_persistent_volume_claim" "db" { 11 | metadata { 12 | name = "db" 13 | } 14 | spec { 15 | access_modes = ["ReadWriteOnce"] 16 | resources { 17 | requests = { 18 | storage = "800Gi" 19 | } 20 | } 21 | 22 | storage_class_name = kubernetes_storage_class.db.metadata.0.name 23 | } 24 | 25 | depends_on = [kubernetes_storage_class.db] 26 | } 27 | 28 | resource "kubernetes_storage_class" "db" { 29 | metadata { 30 | name = "db" 31 | } 32 | 33 | storage_provisioner = "pd.csi.storage.gke.io" 34 | reclaim_policy = "Retain" 35 | volume_binding_mode = "Immediate" 36 | allow_volume_expansion = true 37 | 38 | parameters = { 39 | type = "pd-ssd" 40 | } 41 | } 42 | 43 | #extreme-disk 44 | # resource "google_compute_disk" "extreme-disk" { 45 | # name = "extreme-disk" 46 | # type = "pd-extreme" 47 | # zone = var.zone 48 | # labels = { 49 | # environment = "db" 50 | # } 51 | # #GB 52 | # size = 500 53 | # provisioned_iops = 15000 54 | # depends_on = [google_container_node_pool.db] 55 | # } 56 | 57 | # resource "kubernetes_persistent_volume" "extreme-disk" { 58 | # timeouts { 59 | # create = "3m" 60 | # } 61 | # metadata { 62 | # name = "extreme-disk" 63 | # } 64 | # spec { 65 | # capacity = { 66 | # storage = "500Gi" 67 | # } 68 | # storage_class_name = "pd-extreme" 69 | # access_modes = ["ReadWriteOnce"] 70 | # persistent_volume_source { 71 | # gce_persistent_disk { 72 | # pd_name = google_compute_disk.extreme-disk.name 73 | # fs_type = "ext4" 74 | # } 75 | # } 76 | # } 77 | # depends_on = [google_compute_disk.extreme-disk] 78 | # } 79 | 80 | # resource "kubernetes_persistent_volume_claim" "extreme-disk" { 81 | # timeouts { 82 | # create = "3m" 83 | # } 84 | # metadata { 85 | # name = "extreme-disk" 86 | # } 87 | # spec { 88 | # access_modes = ["ReadWriteOnce"] 89 | # resources { 90 | # requests = { 91 | # storage = "500Gi" 92 | # } 93 | # } 94 | # storage_class_name = "pd-extreme" 95 | # volume_name = kubernetes_persistent_volume.extreme-disk.metadata.0.name 96 | # } 97 | # depends_on = [kubernetes_persistent_volume.extreme-disk] 98 | # } 99 | -------------------------------------------------------------------------------- /src/gcp/config.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | #=================================== 10 | # Default Configurations - No need to adjust 11 | 12 | # at least 2 for multi-connect required 13 | variable "map_web_server_count" { 14 | type = map(number) 15 | default = { 16 | 10 = 2 17 | 20 = 2 18 | 40 = 2 19 | 60 = 2 20 | 80 = 2 21 | 100 = 2 22 | 120 = 2 23 | 160 = 2 24 | 200 = 2 25 | 250 = 3 26 | 300 = 3 27 | } 28 | } 29 | 30 | variable "map_shards_count" { 31 | type = map(number) 32 | default = { 33 | 10 = 2 34 | 20 = 4 35 | 40 = 4 36 | 80 = 8 37 | 120 = 16 38 | 160 = 16 39 | 200 = 32 40 | 250 = 32 41 | 300 = 32 42 | } 43 | } 44 | 45 | # GCP compute instance configuration 46 | variable "map_coreapp_class" { 47 | default = { 48 | "text_or_audio_or_video_or_doc" = { 49 | 10 = "n2-standard-2" 50 | 20 = "n2-standard-2" 51 | 40 = "n2-standard-2" 52 | 80 = "n2-standard-2" 53 | 120 = "n2-standard-2" 54 | 160 = "n2-standard-2" 55 | 200 = "n2-standard-2" 56 | 250 = "n2-standard-2" 57 | 300 = "n2-standard-2" 58 | }, 59 | "image1MB" = { 60 | 10 = "n2-highcpu-8" 61 | 20 = "n2-highcpu-8" 62 | 40 = "n2-highcpu-8" 63 | 80 = "n2-highcpu-8" 64 | 120 = "n2-highcpu-8" 65 | 160 = "n2-highcpu-8" 66 | 200 = "n2-standard-8" 67 | 250 = "n2-standard-16" 68 | 300 = "n2-standard-16" 69 | }, 70 | "image2MB_or_image4MB" = { 71 | 10 = "n2-highcpu-8" 72 | 20 = "n2-highcpu-8" 73 | 40 = "n2-highcpu-8" 74 | 80 = "n2-highcpu-8" 75 | 120 = "n2-standard-8" 76 | 160 = "n2-standard-8" 77 | 200 = "n2-standard-8" 78 | 250 = "n2-standard-16" 79 | 300 = "n2-standard-16" 80 | } 81 | 82 | } 83 | } 84 | 85 | variable "map_db_class" { 86 | type = map(string) 87 | default = { 88 | 10 = "n2-highmem-4" 89 | 20 = "n2-highmem-8" 90 | 40 = "n2-highmem-8" 91 | 80 = "n2-highmem-8" 92 | 120 = "n2-highmem-8" 93 | 160 = "n2-highmem-8" 94 | 200 = "n2-highmem-16" 95 | 250 = "n2-highmem-32" 96 | 300 = "n2-highmem-32" 97 | } 98 | } 99 | 100 | #GCP NFS-pvc sepecific config 101 | variable "nfs-pvc-creation-complete" { 102 | type = bool 103 | default = true 104 | } 105 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | 10 | # Created by https://www.toptal.com/developers/gitignore/api/intellij+all 11 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all 12 | 13 | ### Intellij+all ### 14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 16 | 17 | # User-specific stuff 18 | .idea/**/workspace.xml 19 | .idea/**/tasks.xml 20 | .idea/**/usage.statistics.xml 21 | .idea/**/dictionaries 22 | .idea/**/shelf 23 | 24 | # AWS User-specific 25 | .idea/**/aws.xml 26 | 27 | # Generated files 28 | .idea/**/contentModel.xml 29 | 30 | # Sensitive or high-churn files 31 | .idea/**/dataSources/ 32 | .idea/**/dataSources.ids 33 | .idea/**/dataSources.local.xml 34 | .idea/**/sqlDataSources.xml 35 | .idea/**/dynamic.xml 36 | .idea/**/uiDesigner.xml 37 | .idea/**/dbnavigator.xml 38 | 39 | # Gradle 40 | .idea/**/gradle.xml 41 | .idea/**/libraries 42 | 43 | # Gradle and Maven with auto-import 44 | # When using Gradle or Maven with auto-import, you should exclude module files, 45 | # since they will be recreated, and may cause churn. Uncomment if using 46 | # auto-import. 47 | # .idea/artifacts 48 | # .idea/compiler.xml 49 | # .idea/jarRepositories.xml 50 | # .idea/modules.xml 51 | # .idea/*.iml 52 | # .idea/modules 53 | # *.iml 54 | # *.ipr 55 | 56 | # CMake 57 | cmake-build-*/ 58 | 59 | # Mongo Explorer plugin 60 | .idea/**/mongoSettings.xml 61 | 62 | # File-based project format 63 | *.iws 64 | 65 | # IntelliJ 66 | out/ 67 | 68 | # mpeltonen/sbt-idea plugin 69 | .idea_modules/ 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Cursive Clojure plugin 75 | .idea/replstate.xml 76 | 77 | # SonarLint plugin 78 | .idea/sonarlint/ 79 | 80 | # Crashlytics plugin (for Android Studio and IntelliJ) 81 | com_crashlytics_export_strings.xml 82 | crashlytics.properties 83 | crashlytics-build.properties 84 | fabric.properties 85 | 86 | # Editor-based Rest Client 87 | .idea/httpRequests 88 | 89 | # Android studio 3.1+ serialized cache file 90 | .idea/caches/build_file_checksums.ser 91 | 92 | ### Intellij+all Patch ### 93 | # Ignore everything but code style settings and run configurations 94 | # that are supposed to be shared within teams. 95 | 96 | .idea/* 97 | 98 | !.idea/codeStyles 99 | !.idea/runConfigurations 100 | 101 | # End of https://www.toptal.com/developers/gitignore/api/intellij+all 102 | -------------------------------------------------------------------------------- /src/gcp/k8s-config.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | locals { 10 | db_host_name = "db" 11 | webapp_host_ip = var.nfs-pvc-creation-complete ? kubernetes_service.webapp[0].status.0.load_balancer.0.ingress.0.ip:null 12 | } 13 | 14 | resource "kubernetes_config_map" "mysql-init" { 15 | metadata { 16 | name = "db-user" 17 | } 18 | 19 | data = { 20 | # "add-user.sh" = "${file("scripts/add-user.sh")}" 21 | "copy-cnf.sh" = "${file("scripts/copy-cnf.sh")}" 22 | "my.cnf" = "${file("scripts/my.cnf")}" 23 | } 24 | } 25 | 26 | resource "kubernetes_config_map" "mysql-initdb" { 27 | metadata { 28 | name = "mysql-initdb-config" 29 | } 30 | 31 | data = { 32 | "add-user.sh" = "${file("scripts/add-user.sh")}" 33 | } 34 | } 35 | 36 | resource "kubernetes_config_map" "env" { 37 | metadata { 38 | name = "config-env" 39 | } 40 | 41 | data = { 42 | COREAPP_EXTERNAL_PORTS = "6250,6251,6252,6253" 43 | WA_DB_SSL_CA = var.DBConnCA 44 | WA_DB_PORT = "3306" 45 | WA_DB_HOSTNAME = local.db_host_name 46 | WA_DB_PERSISTENT = "1" 47 | WA_DB_ENGINE = "MYSQL" 48 | WA_CONFIG_ON_DB = "1" 49 | WA_RUNNING_ENV = "GCP" 50 | WA_APP_MULTICONNECT = "1" 51 | WA_DB_CONNECTION_IDLE_TIMEOUT = "180000" 52 | } 53 | } 54 | 55 | resource "kubernetes_config_map" "master" { 56 | metadata { 57 | name = "config-master" 58 | } 59 | 60 | data = { 61 | WA_MASTER_NODE = "1" 62 | } 63 | } 64 | 65 | resource "kubernetes_config_map" "mon-prom" { 66 | count = var.nfs-pvc-creation-complete ? 1 : 0 67 | metadata { 68 | name = "config-mon-prom" 69 | } 70 | 71 | data = { 72 | WA_WEB_ENDPOINT = "${local.webapp_host_ip}:443" 73 | WA_WEB_USERNAME = var.mon-web-username 74 | WA_MYSQLD_EXPORTER_ENDPOINT = "mysqld-exporter:9104" 75 | WA_CORE_ENDPOINT = local.webapp_host_ip 76 | WA_NODE_EXPORTER_PORT = "9100" 77 | WA_CADVISOR_PORT = "8080" 78 | WA_PROMETHEUS_STORAGE_TSDB_RETENTION = "15d" 79 | } 80 | } 81 | 82 | resource "kubernetes_config_map" "mon-graf" { 83 | metadata { 84 | name = "config-mon-graf" 85 | } 86 | data = { 87 | WA_PROMETHEUS_ENDPOINT = "http://prometheus:9090" 88 | GF_SMTP_ENABLED = var.mon-smtp-enabled 89 | GF_SMTP_HOST = var.mon-smtp-host 90 | GF_SMTP_USER = var.mon-smtp-username 91 | GF_SMTP_SKIP_VERIFY = "1" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/gcp/gke.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | provider "google" { 10 | project = var.project_id 11 | region = var.region 12 | } 13 | 14 | provider "google-beta" { 15 | project = var.project_id 16 | region = var.region 17 | } 18 | 19 | data "google_client_config" "default" { 20 | } 21 | 22 | provider "kubernetes" { 23 | config_path = "~/.kube/config" 24 | host = "https://${google_container_cluster.waApi.endpoint}" 25 | 26 | token = data.google_client_config.default.access_token 27 | cluster_ca_certificate = base64decode(google_container_cluster.waApi.master_auth.0.cluster_ca_certificate) 28 | } 29 | 30 | resource "google_container_cluster" "waApi" { 31 | name = "${var.project_id}-${var.name-prefix}-gke" 32 | location = var.zone 33 | 34 | # We can't create a cluster with no node pool defined, but we want to only use 35 | # separately managed node pools. So we create the smallest possible default 36 | # node pool and immediately delete it. 37 | remove_default_node_pool = true 38 | initial_node_count = 3 39 | } 40 | 41 | resource "google_container_node_pool" "coreapp" { 42 | name = "coreapp-node-pool" 43 | location = var.zone 44 | cluster = google_container_cluster.waApi.name 45 | node_count = var.map_shards_count[var.throughput] + 2 46 | 47 | node_config { 48 | preemptible = false 49 | machine_type = var.map_coreapp_class[var.message_type][var.throughput] 50 | 51 | # Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles. 52 | oauth_scopes = [ 53 | "https://www.googleapis.com/auth/cloud-platform", 54 | ] 55 | 56 | labels = { 57 | type = "coreapp" 58 | } 59 | 60 | tags = [var.owner] 61 | 62 | metadata = { 63 | disable-legacy-endpoints = "true" 64 | } 65 | } 66 | } 67 | 68 | resource "google_container_node_pool" "webapp" { 69 | name = "webapp-node-pool" 70 | location = var.zone 71 | cluster = google_container_cluster.waApi.name 72 | node_count = var.map_web_server_count[var.throughput] + 1 73 | 74 | node_config { 75 | preemptible = false 76 | machine_type = var.map_coreapp_class[var.message_type][var.throughput] 77 | # Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles. 78 | oauth_scopes = [ 79 | "https://www.googleapis.com/auth/cloud-platform", 80 | ] 81 | 82 | labels = { 83 | type = "webapp" 84 | } 85 | tags = [var.owner] 86 | 87 | metadata = { 88 | disable-legacy-endpoints = "true" 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/azure/network.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | 10 | resource "azurerm_virtual_network" "waNet" { 11 | name = module.naming.virtual_network.name_unique 12 | location = azurerm_resource_group.waNet.location 13 | resource_group_name = azurerm_resource_group.waNet.name 14 | address_space = ["10.0.0.0/16"] 15 | } 16 | 17 | resource "azurerm_subnet" "subnet-main" { 18 | name = "${module.naming.subnet.name_unique}-main" 19 | resource_group_name = azurerm_resource_group.waNet.name 20 | address_prefixes = ["10.0.128.0/20"] 21 | virtual_network_name = azurerm_virtual_network.waNet.name 22 | service_endpoints = ["Microsoft.Storage", "Microsoft.Sql"] 23 | enforce_private_link_endpoint_network_policies = true 24 | } 25 | 26 | resource "azurerm_subnet" "subnet-ha" { 27 | name = "${module.naming.subnet.name_unique}-ha" 28 | resource_group_name = azurerm_resource_group.waNet.name 29 | address_prefixes = ["10.0.144.0/20"] 30 | virtual_network_name = azurerm_virtual_network.waNet.name 31 | enforce_private_link_endpoint_network_policies = true 32 | } 33 | 34 | # for flexible server 35 | resource "azurerm_subnet" "subnet-main-db" { 36 | name = "${module.naming.subnet.name_unique}-main-db" 37 | resource_group_name = azurerm_resource_group.waNet.name 38 | virtual_network_name = azurerm_virtual_network.waNet.name 39 | address_prefixes = ["10.0.160.0/24"] 40 | service_endpoints = ["Microsoft.Storage"] 41 | delegation { 42 | name = "fs" 43 | service_delegation { 44 | name = "Microsoft.DBforMySQL/flexibleServers" 45 | actions = [ 46 | "Microsoft.Network/virtualNetworks/subnets/join/action", 47 | ] 48 | } 49 | } 50 | } 51 | 52 | #for single server and vm+mysql 53 | resource "azurerm_subnet" "subnet-main-db-ss" { 54 | name = "${module.naming.subnet.name_unique}-main-db-ss" 55 | resource_group_name = azurerm_resource_group.waNet.name 56 | virtual_network_name = azurerm_virtual_network.waNet.name 57 | address_prefixes = ["10.0.161.0/24"] 58 | service_endpoints = ["Microsoft.Storage"] 59 | enforce_private_link_endpoint_network_policies = true 60 | } 61 | 62 | resource "azurerm_network_watcher" "waNet" { 63 | name = "production-nwwatcher" 64 | location = azurerm_resource_group.waNet.location 65 | resource_group_name = azurerm_resource_group.waNet.name 66 | } 67 | -------------------------------------------------------------------------------- /src/azure/k8s-config.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | locals { 10 | # db_host_name = azurerm_mysql_flexible_server.waApi.fqdn #flexible server 11 | # db_host_name = azurerm_mysql_server.waApi.fqdn #single server 12 | db_host_name = kubernetes_service.db.metadata.0.name #vm server 13 | } 14 | resource "kubernetes_config_map" "env" { 15 | metadata { 16 | name = "config-env" 17 | # namespace = kubernetes_namespace.deployment.metadata.0.name 18 | } 19 | 20 | data = { 21 | COREAPP_EXTERNAL_PORTS = "6250,6251,6252,6253" 22 | WA_DB_SSL_CA = var.DBConnCA 23 | WA_DB_PORT = "3306" 24 | # COREAPP_HOSTNAME = "" 25 | WA_DB_HOSTNAME = local.db_host_name 26 | WA_DB_PERSISTENT = "1" 27 | WA_DB_ENGINE = "MYSQL" 28 | WA_CONFIG_ON_DB = "1" 29 | WA_RUNNING_ENV = "AZURE" 30 | WA_APP_MULTICONNECT = "1" 31 | WA_DB_CONNECTION_IDLE_TIMEOUT = "180000" 32 | } 33 | } 34 | 35 | resource "kubernetes_config_map" "mon-prom" { 36 | metadata { 37 | name = "config-mon-prom" 38 | # namespace = kubernetes_namespace.deployment.metadata.0.name 39 | } 40 | 41 | data = { 42 | WA_WEB_ENDPOINT = "${azurerm_public_ip.waApi.fqdn}:443" 43 | WA_WEB_USERNAME = var.wabiz-web-username 44 | WA_MYSQLD_EXPORTER_ENDPOINT = "mysqld-exporter:9104" 45 | WA_CORE_ENDPOINT = azurerm_public_ip.waApi.fqdn 46 | WA_NODE_EXPORTER_PORT = "9100" 47 | WA_CADVISOR_PORT = "8080" 48 | WA_PROMETHEUS_STORAGE_TSDB_RETENTION = "15d" 49 | } 50 | } 51 | 52 | resource "kubernetes_config_map" "mon-graf" { 53 | metadata { 54 | name = "config-mon-graf" 55 | # namespace = kubernetes_namespace.deployment.metadata.0.name 56 | } 57 | 58 | data = { 59 | WA_PROMETHEUS_ENDPOINT = "http://prometheus:9090" 60 | GF_SMTP_ENABLED = var.mon-smtp-enabled 61 | GF_SMTP_HOST = var.mon-smtp-host 62 | GF_SMTP_USER = var.mon-smtp-username 63 | GF_SMTP_SKIP_VERIFY = "1" 64 | } 65 | } 66 | 67 | resource "kubernetes_config_map" "master" { 68 | metadata { 69 | name = "config-master" 70 | # namespace = kubernetes_namespace.deployment.metadata.0.name 71 | } 72 | 73 | data = { 74 | WA_MASTER_NODE = "1" 75 | } 76 | } 77 | 78 | resource "kubernetes_config_map" "mysql-init" { 79 | metadata { 80 | name = "db-user" 81 | # namespace = kubernetes_namespace.deployment.metadata.0.name 82 | } 83 | 84 | data = { 85 | "copy-cnf.sh" = "${file("scripts/copy-cnf.sh")}" 86 | "my.cnf" = "${file("scripts/my.cnf")}" 87 | } 88 | } 89 | 90 | resource "kubernetes_config_map" "mysql-initdb" { 91 | metadata { 92 | name = "mysql-initdb-config" 93 | } 94 | 95 | data = { 96 | "add-user.sh" = "${file("scripts/add-user.sh")}" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/azure/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | 10 | # Configure the Azure provider 11 | terraform { 12 | required_providers { 13 | azurerm = { 14 | source = "hashicorp/azurerm" 15 | version = "=2.96.0" 16 | } 17 | } 18 | 19 | required_version = ">= 1.1.0" 20 | } 21 | 22 | provider "azurerm" { 23 | features {} 24 | } 25 | 26 | provider "kubernetes" { 27 | config_path = "~/.kube/config" 28 | host = azurerm_kubernetes_cluster.waApi.kube_config.0.host 29 | client_certificate = base64decode(azurerm_kubernetes_cluster.waApi.kube_config.0.client_certificate) 30 | client_key = base64decode(azurerm_kubernetes_cluster.waApi.kube_config.0.client_key) 31 | cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.waApi.kube_config.0.cluster_ca_certificate) 32 | } 33 | 34 | resource "random_id" "name" { 35 | keepers = { 36 | owner = "${var.owner}" 37 | } 38 | byte_length = 8 39 | } 40 | 41 | module "naming" { 42 | source = "Azure/naming/azurerm" 43 | suffix = ["${var.name-prefix}", "${var.owner}"] 44 | unique-seed = random_id.name.hex 45 | } 46 | 47 | resource "azurerm_resource_group" "waNet" { 48 | name = module.naming.resource_group.name 49 | location = var.location 50 | tags = { 51 | "owner" = var.owner 52 | } 53 | } 54 | 55 | resource "azurerm_resource_group" "waApi" { 56 | name = module.naming.resource_group.name 57 | location = var.location 58 | tags = { 59 | "owner" = var.owner 60 | } 61 | } 62 | 63 | resource "azurerm_role_assignment" "waApi" { 64 | scope = azurerm_resource_group.waApi.id 65 | role_definition_name = "Contributor" 66 | principal_id = azurerm_kubernetes_cluster.waApi.identity[0].principal_id 67 | } 68 | 69 | resource "azurerm_log_analytics_workspace" "waApi" { 70 | name = module.naming.log_analytics_workspace.name 71 | location = azurerm_resource_group.waApi.location 72 | resource_group_name = azurerm_resource_group.waApi.name 73 | sku = "standalone" 74 | retention_in_days = 30 75 | } 76 | 77 | output "web_server_name" { 78 | value = azurerm_public_ip.waApi.fqdn 79 | } 80 | 81 | output "monitor_server_name" { 82 | value = azurerm_public_ip.waMon.fqdn 83 | } 84 | 85 | output "number_of_shards" { 86 | value = var.map_shards_count[var.throughput] 87 | } 88 | 89 | # output "db_server_name" { 90 | # value = azurerm_mysql_flexible_server.waApi.fqdn 91 | # } 92 | 93 | # output "db_server_name_single_server" { 94 | # value = azurerm_mysql_server.waApi.fqdn 95 | # } 96 | 97 | # output "kube_config" { 98 | # value = azurerm_kubernetes_cluster.waApi.kube_config_raw 99 | 100 | # sensitive = true 101 | # } 102 | 103 | # output "client_certificate" { 104 | # value = azurerm_kubernetes_cluster.waApi.kube_config.0.client_certificate 105 | # } 106 | -------------------------------------------------------------------------------- /src/gcp/k8s-media.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | # provider "kubectl" { 11 | # config_path = "~/.kube/config" 12 | # host = "https://${google_container_cluster.waApi.endpoint}" 13 | # username = var.gke_username 14 | # password = var.gke_password 15 | # client_certificate = base64decode(google_container_cluster.waApi.master_auth.0.client_certificate) 16 | # client_key = base64decode(google_container_cluster.waApi.master_auth.0.client_key) 17 | # cluster_ca_certificate = base64decode(google_container_cluster.waApi.master_auth.0.cluster_ca_certificate) 18 | # } 19 | 20 | # #Use raw yaml due to issue in terraform: https://github.com/hashicorp/terraform-provider-kubernetes/issues/1379?fbclid=IwAR2k0P0YSI1Uw8XNJfCfdNoqn3GAJjgDD1r6buRuvSUOss-uoG5pmi6Wr30 21 | # resource "kubectl_manifest" "media_share_pv" { 22 | # yaml_body = < 250 MPS. Please use at your own discretion.* 24 | 25 | ## Technologies 26 | ### AWS: 27 | * Infra definition: [CloudFormation](https://docs.aws.amazon.com/cloudformation/index.html) 28 | * Database: [Amazon Aurora](https://aws.amazon.com/rds/aurora/) 29 | * Container management: [Amazon Elastic Container Service (Amazon ECS)](https://aws.amazon.com/ecs/) 30 | ### Azure: 31 | * Infra definition: [Terraform](https://www.terraform.io/) 32 | * Database: MySQL in VM 33 | * Container management: [Kubernetes](https://kubernetes.io/) 34 | ### GCP: 35 | * Infra definition: [Terraform](https://www.terraform.io/) 36 | * Database: MySQL in VM 37 | * Container management: [Kubernetes](https://kubernetes.io/) 38 | 39 | ## Get Started 40 | 1. Clone or download files based on your cloud platform from the `src` directory. 41 | 2. Follow the step-by-step guide below to deploy the templates based on your desired throughput and message type: 42 | * AWS: https://developers.facebook.com/docs/whatsapp/on-premises/get-started/installation/aws 43 | * Azure: https://developers.facebook.com/docs/whatsapp/on-premises/get-started/installation/azure 44 | * GCP: https://developers.facebook.com/docs/whatsapp/on-premises/get-started/installation/gcp 45 | 46 | ## Support 47 | We highly recommend you to deploy the templates in a testing environment first before deploying in production. We have verified the templates under the conditions documented in the [Benchmark Results](#benchmark-results) section, however, we are unable to guarantee the maximum throughput if alterations are made to the templates. 48 | 49 | If you have any feedback on how to improve the templates, please kindly file an issue or a pull request in this repository. We will follow up on them in our best efforts. 50 | 51 | For any other types of issues, please contact Direct Support. 52 | 53 | ## License 54 | WhatsApp Business API (On-Premises) Deployment Templates is [MIT licensed](./LICENSE). 55 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /src/gcp/variables.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | # General Configuration 11 | variable "name-prefix" { 12 | default = "" 13 | } 14 | 15 | # Filling out before you start 16 | variable "project_id" { 17 | default = "" 18 | description = "project id" 19 | 20 | validation { 21 | condition = length(var.project_id) > 0 22 | error_message = "Project ID cannot be empty." 23 | } 24 | } 25 | 26 | variable "region" { 27 | default = "us-west1" 28 | description = "region" 29 | } 30 | 31 | variable "zone" { 32 | default = "us-west1-a" 33 | description = "zone" 34 | } 35 | 36 | variable "owner" { 37 | default = "meta" 38 | description = "Owner" 39 | } 40 | 41 | # Throughput Configuration 42 | variable "throughput" { 43 | type = number 44 | default = 300 45 | 46 | validation { 47 | condition = contains([10, 20, 40, 60, 80, 100, 120,160,200,250,300], var.throughput) 48 | error_message = "Valid values var.throughput are: 10, 20, 40, 60, 80, 100, 120, 160, 200, 250, 300." 49 | } 50 | } 51 | 52 | variable "message_type" { 53 | type = string 54 | default = "image2MB_or_image4MB" 55 | 56 | validation { 57 | condition = contains(["text_or_audio_or_video_or_doc", "image1MB", "image2MB_or_image4MB"], var.message_type) 58 | error_message = "Valid values for var.messageType are: text, audio, video, doc, image1MB, image2MB, image4MB." 59 | } 60 | } 61 | 62 | # WhatsApp Business API Configuration 63 | variable "api-version" { 64 | default = "v2.45.2" 65 | } 66 | 67 | # Database Configuration 68 | variable "dbusername" { 69 | default = "dbadmin" 70 | } 71 | 72 | variable "dbpassword" { 73 | type = string 74 | description = "Database admin user password" 75 | validation { 76 | condition = length(var.dbpassword) > 0 77 | error_message = "Database admin user password cannot be empty. Should NOT contain any of these characters: ?{}&~!()^=" 78 | } 79 | default = "" 80 | } 81 | 82 | variable "DBCertURL" { 83 | default = "" # mysql in vm 84 | # default = "https://dl.cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem" # flexible server 85 | # default = "https://www.digicert.com/CACerts/BaltimoreCyberTrustRoot.crt.pem" # single server 86 | } 87 | 88 | variable "DBConnCA" { 89 | default = "/opt/certs/db-ca.pem" 90 | } 91 | 92 | # Grafana Configuration 93 | variable "mon-web-username" { 94 | default = "admin" 95 | } 96 | 97 | #Login in password 98 | variable "mon-web-password" { 99 | default = "" 100 | description = "Set the Grafana dashboard login password" 101 | validation { 102 | condition = length(var.mon-web-password) > 0 103 | error_message = "Grafana admin user password cannot be empty." 104 | } 105 | } 106 | 107 | variable "mon-smtp-enabled" { 108 | default = "0" 109 | } 110 | 111 | variable "mon-smtp-host" { 112 | default = "" 113 | } 114 | 115 | variable "mon-smtp-username" { 116 | default = "admin" 117 | } 118 | 119 | variable "mon-smtp-password" { 120 | default = "" 121 | } 122 | 123 | #!!!!!Need to match Postman API user and password!!! 124 | variable "wabiz-web-username" { 125 | default = "admin" 126 | } 127 | 128 | variable "wabiz-web-password" { 129 | type = string 130 | description = "WhatsApp Business API Password" 131 | validation { 132 | condition = length(var.wabiz-web-password) >= 8 && length(var.wabiz-web-password) <= 64 133 | error_message = "Password needs to be 8-64 characters long with at least 1 digit, 1 uppercase letter, 1 lowercase letter and 1 special character" 134 | } 135 | default = "" 136 | } 137 | -------------------------------------------------------------------------------- /src/azure/k8s-service.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | 10 | resource "kubernetes_service" "webapp" { 11 | timeouts { 12 | create = "2m" 13 | } 14 | metadata { 15 | name = "webapp" 16 | labels = { type = "webapp" } 17 | } 18 | spec { 19 | selector = { 20 | type = "webapp" 21 | } 22 | 23 | load_balancer_ip = azurerm_public_ip.waApi.ip_address 24 | 25 | port { 26 | port = 443 27 | target_port = 443 28 | } 29 | 30 | type = "LoadBalancer" 31 | } 32 | depends_on = [kubernetes_deployment.webapp] 33 | } 34 | 35 | resource "kubernetes_service" "exporter-coreapp" { 36 | metadata { 37 | name = "exporter-coreapp" 38 | labels = { type = "monitor" } 39 | } 40 | spec { 41 | selector = { 42 | type = "coreapp" 43 | } 44 | 45 | port { 46 | name = "cadvisor" 47 | port = 8080 48 | target_port = 8080 49 | } 50 | 51 | port { 52 | name = "node-exporter" 53 | port = 9100 54 | target_port = 9100 55 | } 56 | } 57 | depends_on = [kubernetes_deployment.monitor] 58 | } 59 | 60 | resource "kubernetes_service" "exporter-webapp" { 61 | metadata { 62 | name = "exporter-webapp" 63 | labels = { type = "monitor" } 64 | } 65 | spec { 66 | selector = { 67 | type = "webapp" 68 | } 69 | 70 | port { 71 | name = "cadvisor" 72 | port = 8080 73 | target_port = 8080 74 | } 75 | 76 | port { 77 | name = "node-exporter" 78 | port = 9100 79 | target_port = 9100 80 | } 81 | } 82 | depends_on = [kubernetes_deployment.monitor] 83 | } 84 | 85 | resource "kubernetes_service" "mysqld-exporter" { 86 | metadata { 87 | name = "mysqld-exporter" 88 | labels = { type = "monitor" } 89 | } 90 | spec { 91 | selector = { 92 | type = "monitor" 93 | } 94 | 95 | port { 96 | name = "mysqld-exporter" 97 | port = 9104 98 | target_port = 9104 99 | } 100 | } 101 | depends_on = [kubernetes_deployment.monitor] 102 | } 103 | 104 | resource "kubernetes_service" "prometheus" { 105 | metadata { 106 | name = "prometheus" 107 | labels = { type = "monitor" } 108 | } 109 | spec { 110 | selector = { 111 | type = "monitor" 112 | } 113 | 114 | port { 115 | name = "prometheus" 116 | port = 9090 117 | target_port = 9090 118 | } 119 | } 120 | depends_on = [kubernetes_deployment.monitor] 121 | } 122 | 123 | resource "kubernetes_service" "monitor" { 124 | timeouts { 125 | create = "2m" 126 | } 127 | metadata { 128 | name = "monitor" 129 | labels = { type = "monitor" } 130 | } 131 | spec { 132 | selector = { 133 | type = "monitor" 134 | } 135 | 136 | load_balancer_ip = azurerm_public_ip.waMon.ip_address 137 | 138 | port { 139 | name = "grafana" 140 | port = 3000 141 | target_port = 3000 142 | } 143 | 144 | port { 145 | name = "prometheus" 146 | port = 9090 147 | target_port = 9090 148 | } 149 | 150 | type = "LoadBalancer" 151 | } 152 | 153 | depends_on = [kubernetes_deployment.monitor] 154 | } 155 | 156 | resource "kubernetes_service" "db-headless" { 157 | metadata { 158 | name = "db-headless" 159 | labels = { type = "db" } 160 | } 161 | spec { 162 | selector = { 163 | type = "db" 164 | } 165 | 166 | cluster_ip = "None" 167 | 168 | port { 169 | port = 3306 170 | target_port = 3306 171 | } 172 | } 173 | } 174 | 175 | resource "kubernetes_service" "db" { 176 | metadata { 177 | name = "db" 178 | labels = { type = "db" } 179 | } 180 | spec { 181 | selector = { 182 | type = "db" 183 | } 184 | 185 | port { 186 | name = "db" 187 | port = 3306 188 | target_port = 3306 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/gcp/nfs.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | resource "google_container_node_pool" "nfs" { 11 | name = "nfs-node-pool" 12 | location = var.zone 13 | cluster = google_container_cluster.waApi.name 14 | node_count = 2 15 | 16 | node_config { 17 | preemptible = false 18 | machine_type = var.map_coreapp_class[var.message_type][var.throughput] 19 | oauth_scopes = [ 20 | "https://www.googleapis.com/auth/cloud-platform", 21 | ] 22 | 23 | labels = { 24 | type = "nfs" 25 | } 26 | tags = [var.owner] 27 | 28 | metadata = { 29 | disable-legacy-endpoints = "true" 30 | } 31 | } 32 | } 33 | 34 | resource "kubernetes_persistent_volume_claim" "nfs_store" { 35 | metadata { 36 | name = "nfs-store" 37 | } 38 | spec { 39 | access_modes = ["ReadWriteOnce"] 40 | resources { 41 | requests = { 42 | storage = "200Gi" 43 | } 44 | } 45 | storage_class_name = kubernetes_storage_class.nfs_store.metadata.0.name 46 | } 47 | 48 | depends_on = [kubernetes_storage_class.nfs_store] 49 | } 50 | 51 | resource "kubernetes_storage_class" "nfs_store" { 52 | metadata { 53 | name = "nfs-store" 54 | } 55 | 56 | storage_provisioner = "pd.csi.storage.gke.io" 57 | reclaim_policy = "Retain" 58 | volume_binding_mode = "Immediate" 59 | allow_volume_expansion = true 60 | 61 | parameters = { 62 | type = "pd-ssd" 63 | } 64 | } 65 | 66 | resource "kubernetes_deployment" "nfs_server" { 67 | metadata { 68 | name = "nfs-server" 69 | } 70 | 71 | spec { 72 | replicas = 1 73 | 74 | selector { 75 | match_labels = { 76 | type = "nfs-server" 77 | } 78 | } 79 | 80 | template { 81 | metadata { 82 | labels = { 83 | type= "nfs-server" 84 | } 85 | } 86 | 87 | spec { 88 | 89 | volume { 90 | name = "nfsstore" 91 | 92 | persistent_volume_claim { 93 | claim_name = kubernetes_persistent_volume_claim.nfs_store.metadata.0.name 94 | } 95 | } 96 | 97 | affinity { 98 | pod_anti_affinity { 99 | required_during_scheduling_ignored_during_execution { 100 | label_selector { 101 | match_expressions { 102 | key = "type" 103 | operator = "In" 104 | values = ["nfs-server"] 105 | } 106 | } 107 | topology_key = "kubernetes.io/hostname" 108 | } 109 | } 110 | node_affinity { 111 | required_during_scheduling_ignored_during_execution { 112 | node_selector_term { 113 | match_expressions { 114 | key = "type" 115 | operator = "In" 116 | values = ["nfs"] 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | container { 124 | name = "nfs-server" 125 | image = "gcr.io/google_containers/volume-nfs:0.8" 126 | 127 | port { 128 | name = "nfs" 129 | container_port = 2049 130 | } 131 | 132 | port { 133 | name = "mountd" 134 | container_port = 20048 135 | } 136 | 137 | port { 138 | name = "rpcbind" 139 | container_port = 111 140 | } 141 | 142 | volume_mount { 143 | name = "nfsstore" 144 | mount_path = "/exports" 145 | } 146 | 147 | security_context { 148 | privileged = true 149 | } 150 | } 151 | } 152 | } 153 | } 154 | depends_on = [google_container_node_pool.nfs] 155 | } 156 | 157 | 158 | resource "kubernetes_service" "nfs_server" { 159 | metadata { 160 | name = "nfs-server" 161 | } 162 | 163 | spec { 164 | port { 165 | name = "nfs" 166 | port = 2049 167 | } 168 | 169 | port { 170 | name = "mountd" 171 | port = 20048 172 | } 173 | 174 | port { 175 | name = "rpcbind" 176 | port = 111 177 | } 178 | 179 | selector = { 180 | type = "nfs-server" 181 | } 182 | } 183 | depends_on = [kubernetes_deployment.nfs_server] 184 | } 185 | -------------------------------------------------------------------------------- /src/gcp/k8s-db.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | resource "google_container_node_pool" "db" { 11 | name = "db-node-pool" 12 | location = var.zone 13 | cluster = google_container_cluster.waApi.name 14 | node_count = 2 15 | 16 | node_config { 17 | preemptible = false 18 | machine_type = var.map_db_class[var.throughput] 19 | 20 | # Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles. 21 | oauth_scopes = [ 22 | "https://www.googleapis.com/auth/cloud-platform", 23 | ] 24 | 25 | labels = { 26 | type = "db" 27 | } 28 | 29 | tags = [var.owner] 30 | 31 | metadata = { 32 | disable-legacy-endpoints = "true" 33 | } 34 | } 35 | } 36 | 37 | 38 | resource "kubernetes_stateful_set" "db" { 39 | timeouts { 40 | create = "8m" 41 | update = "1m" 42 | } 43 | metadata { 44 | name = "db" 45 | labels = { 46 | type = "db" 47 | } 48 | } 49 | 50 | spec { 51 | pod_management_policy = "Parallel" 52 | replicas = 1 53 | revision_history_limit = 5 54 | 55 | selector { 56 | match_labels = { 57 | type = "db" 58 | } 59 | } 60 | 61 | service_name = "db" 62 | template { 63 | metadata { 64 | labels = { 65 | type = "db" 66 | } 67 | 68 | annotations = {} 69 | } 70 | 71 | spec { 72 | affinity { 73 | node_affinity { 74 | required_during_scheduling_ignored_during_execution { 75 | node_selector_term { 76 | match_expressions { 77 | key = "type" 78 | operator = "In" 79 | values = ["db"] 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | container { 87 | name = "db-server" 88 | image = "mysql:8.0.34" 89 | image_pull_policy = "IfNotPresent" 90 | 91 | 92 | lifecycle { 93 | post_start { 94 | exec { 95 | command = ["/bin/bash", "-c", "/var/mysql/init/copy-cnf.sh"] 96 | # command = ["/bin/bash", "-c", "/var/mysql/init/add-user.sh"] 97 | } 98 | } 99 | } 100 | 101 | port { 102 | container_port = 3306 103 | } 104 | 105 | env { 106 | name = "MYSQL_ROOT_PASSWORD" 107 | value = var.dbpassword 108 | } 109 | 110 | env_from { 111 | secret_ref { 112 | name = local.secret_map_ref_name 113 | } 114 | } 115 | 116 | volume_mount { 117 | name = local.db_vol 118 | mount_path = local.db_mount_path 119 | sub_path = local.db_sub_path 120 | } 121 | 122 | volume_mount { 123 | name = local.db_vol 124 | mount_path = local.db_config_mount_path 125 | sub_path = local.db_config_sub_path 126 | } 127 | 128 | volume_mount { 129 | name = local.mysql_init_vol 130 | mount_path = local.mysql_init_mount_path 131 | } 132 | 133 | volume_mount { 134 | name = "mysql-initdb" 135 | mount_path = "/docker-entrypoint-initdb.d" 136 | } 137 | } 138 | 139 | termination_grace_period_seconds = 300 140 | 141 | volume { 142 | name = local.db_vol 143 | persistent_volume_claim { 144 | # claim_name = google_compute_disk.extreme-disk.name 145 | claim_name = kubernetes_persistent_volume_claim.db.metadata.0.name 146 | } 147 | } 148 | 149 | volume { 150 | name = local.mysql_init_vol 151 | config_map { 152 | name = kubernetes_config_map.mysql-init.metadata[0].name 153 | default_mode = "0777" 154 | } 155 | } 156 | 157 | volume { 158 | name = "mysql-initdb" 159 | config_map { 160 | name = kubernetes_config_map.mysql-initdb.metadata.0.name 161 | default_mode = "0777" 162 | } 163 | } 164 | } 165 | } 166 | 167 | update_strategy { 168 | type = "RollingUpdate" 169 | 170 | rolling_update { 171 | partition = 1 172 | } 173 | } 174 | } 175 | 176 | depends_on = [google_container_node_pool.db, kubernetes_persistent_volume_claim.db] 177 | } 178 | 179 | resource "kubernetes_service" "db-headless" { 180 | metadata { 181 | name = "db-headless" 182 | labels = { type = "db" } 183 | } 184 | spec { 185 | selector = { 186 | type = "db" 187 | } 188 | 189 | cluster_ip = "None" 190 | 191 | port { 192 | port = 3306 193 | target_port = 3306 194 | } 195 | } 196 | } 197 | 198 | resource "kubernetes_service" "db" { 199 | metadata { 200 | name = "db" 201 | labels = { type = "db" } 202 | } 203 | spec { 204 | selector = { 205 | type = "db" 206 | } 207 | 208 | port { 209 | name = "db" 210 | port = 3306 211 | target_port = 3306 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/azure/k8s-compute.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | resource "azurerm_kubernetes_cluster" "waApi" { 10 | name = module.naming.kubernetes_cluster.name_unique 11 | location = azurerm_resource_group.waApi.location 12 | resource_group_name = azurerm_resource_group.waApi.name 13 | node_resource_group = "${module.naming.resource_group.name_unique}-aks-node" 14 | dns_prefix = "${var.name-prefix}-${var.owner}" 15 | 16 | default_node_pool { 17 | availability_zones = [ 18 | "1", 19 | ] 20 | enable_auto_scaling = true 21 | enable_host_encryption = false 22 | enable_node_public_ip = false 23 | fips_enabled = false 24 | kubelet_disk_type = "OS" 25 | max_count = 1 26 | max_pods = 30 27 | min_count = 1 28 | name = "sys" 29 | node_count = 1 30 | only_critical_addons_enabled = false 31 | os_disk_size_gb = 128 32 | os_disk_type = "Managed" 33 | os_sku = "Ubuntu" 34 | tags = { 35 | "owner" = var.owner 36 | } 37 | type = "VirtualMachineScaleSets" 38 | ultra_ssd_enabled = false 39 | vm_size = var.k8s-vm-class 40 | vnet_subnet_id = azurerm_subnet.subnet-main.id 41 | } 42 | 43 | network_profile { 44 | network_plugin = "azure" 45 | service_cidr = "10.0.176.0/20" 46 | docker_bridge_cidr = "10.0.176.0/20" 47 | dns_service_ip = "10.0.176.3" 48 | } 49 | 50 | identity { 51 | type = "SystemAssigned" 52 | } 53 | 54 | linux_profile { 55 | admin_username = "azureuser" 56 | ssh_key { 57 | key_data = file(var.ssh-pub-key) 58 | } 59 | } 60 | 61 | tags = { 62 | owner = var.owner 63 | } 64 | } 65 | 66 | resource "azurerm_kubernetes_cluster_node_pool" "db" { 67 | availability_zones = [ 68 | "2", 69 | ] 70 | enable_auto_scaling = false 71 | enable_host_encryption = false 72 | enable_node_public_ip = false 73 | fips_enabled = false 74 | kubelet_disk_type = "OS" 75 | kubernetes_cluster_id = azurerm_kubernetes_cluster.waApi.id 76 | max_count = 0 77 | max_pods = 30 78 | min_count = 0 79 | mode = "User" 80 | name = "db" 81 | node_count = 2 82 | node_labels = { "type" : "db" } 83 | os_disk_size_gb = 32 84 | os_disk_type = "Managed" 85 | os_sku = "Ubuntu" 86 | os_type = "Linux" 87 | priority = "Regular" 88 | scale_down_mode = "Delete" 89 | spot_max_price = -1 90 | ultra_ssd_enabled = true 91 | vm_size = var.map_db_class[var.throughput] 92 | vnet_subnet_id = azurerm_subnet.subnet-main-db-ss.id 93 | 94 | tags = { 95 | owner = var.owner 96 | } 97 | 98 | } 99 | 100 | resource "azurerm_kubernetes_cluster_node_pool" "coreapp" { 101 | availability_zones = [ 102 | "1", 103 | ] 104 | enable_auto_scaling = false 105 | enable_host_encryption = false 106 | enable_node_public_ip = true 107 | fips_enabled = false 108 | kubelet_disk_type = "OS" 109 | kubernetes_cluster_id = azurerm_kubernetes_cluster.waApi.id 110 | max_count = 0 111 | max_pods = 30 112 | min_count = 0 113 | mode = "User" 114 | name = "coreapp" 115 | node_count = var.map_shards_count[var.throughput] + 1 116 | node_labels = { "type" : "coreapp" } 117 | os_disk_size_gb = 32 118 | os_disk_type = "Managed" 119 | os_sku = "Ubuntu" 120 | os_type = "Linux" 121 | priority = "Regular" 122 | scale_down_mode = "Delete" 123 | spot_max_price = -1 124 | ultra_ssd_enabled = false 125 | vm_size = var.map_coreapp_class[var.message_type][var.throughput] 126 | vnet_subnet_id = azurerm_subnet.subnet-main.id 127 | 128 | tags = { 129 | owner = var.owner 130 | } 131 | 132 | # lifecycle { 133 | # ignore_changes = [node_count] 134 | # } 135 | 136 | depends_on = [ 137 | azurerm_kubernetes_cluster_node_pool.db, azurerm_kubernetes_cluster_node_pool.webapp 138 | ] 139 | 140 | } 141 | 142 | resource "azurerm_kubernetes_cluster_node_pool" "webapp" { 143 | availability_zones = [ 144 | "1", 145 | ] 146 | enable_auto_scaling = false 147 | enable_host_encryption = false 148 | enable_node_public_ip = true 149 | fips_enabled = false 150 | kubelet_disk_type = "OS" 151 | kubernetes_cluster_id = azurerm_kubernetes_cluster.waApi.id 152 | max_count = 0 153 | max_pods = 30 154 | min_count = 0 155 | mode = "User" 156 | name = "webapp" 157 | node_count = var.map_web_server_count[var.throughput] 158 | node_labels = { "type" : "webapp" } 159 | os_disk_size_gb = 32 160 | os_disk_type = "Managed" 161 | os_sku = "Ubuntu" 162 | os_type = "Linux" 163 | priority = "Regular" 164 | scale_down_mode = "Delete" 165 | spot_max_price = -1 166 | ultra_ssd_enabled = false 167 | vm_size = "Standard_F2s_v2" 168 | vnet_subnet_id = azurerm_subnet.subnet-main.id 169 | 170 | tags = { 171 | owner = var.owner 172 | } 173 | 174 | depends_on = [ 175 | azurerm_kubernetes_cluster_node_pool.db 176 | ] 177 | 178 | } 179 | 180 | resource "azurerm_kubernetes_cluster_node_pool" "monitor" { 181 | availability_zones = [ 182 | "1", 183 | ] 184 | enable_auto_scaling = false 185 | enable_host_encryption = false 186 | enable_node_public_ip = true 187 | fips_enabled = false 188 | kubelet_disk_type = "OS" 189 | kubernetes_cluster_id = azurerm_kubernetes_cluster.waApi.id 190 | max_count = 0 191 | max_pods = 30 192 | min_count = 0 193 | mode = "User" 194 | name = "monitor" 195 | node_count = 1 196 | node_labels = { "type" : "monitor" } 197 | os_disk_size_gb = 128 198 | os_disk_type = "Managed" 199 | os_sku = "Ubuntu" 200 | os_type = "Linux" 201 | priority = "Regular" 202 | scale_down_mode = "Delete" 203 | spot_max_price = -1 204 | ultra_ssd_enabled = false 205 | vm_size = "Standard_B2ms" 206 | vnet_subnet_id = azurerm_subnet.subnet-main.id 207 | 208 | tags = { 209 | owner = var.owner 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/aws/wa_ent_db.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | # 6 | AWSTemplateFormatVersion: 2010-09-09 7 | Description: >- 8 | AWS CloudFormation template to create database(s) required for WhatsApp. 9 | This could be optional too, as customers *might* have RDS in its AWS 10 | infrastructure already. However, this template is provided for reference 11 | to create database instances (RDS-MySQL), if customers do not have DB in 12 | its infrastructure 13 | Note- At least 2 subnets should be selected 14 | Metadata: 15 | AWS::CloudFormation::Interface: 16 | ParameterGroups: 17 | - Label: 18 | default: "Network configuration" 19 | Parameters: 20 | - VpcId 21 | - SubnetIDs 22 | - Label: 23 | default: "Database configuration" 24 | Parameters: 25 | - DBInstanceClass 26 | - DBUser 27 | - DBPassword 28 | - DBPort 29 | - Label: 30 | default: "Security configuration" 31 | Parameters: 32 | - DBEncryptionKeyType 33 | - EncryptionKeyId 34 | 35 | Parameters: 36 | VpcId: 37 | Type: AWS::EC2::VPC::Id 38 | Description: Select a VPC for DB 39 | SubnetIDs: 40 | Type: List 41 | Description: Select subnets for DB. Subnets must be in same VPC 42 | DBEncryptionKeyType: 43 | Description: Please choose key type for DB encryption 44 | Type: String 45 | Default: Default-Key 46 | AllowedValues: 47 | - Unencrypted 48 | - Default-Key 49 | - Create-New-Key 50 | - User-Provided-Key 51 | EncryptionKeyId: 52 | Description: Provide encryption key id 53 | Type: String 54 | Default: "" 55 | DBUser: 56 | NoEcho: "true" 57 | Description: The database admin account username 58 | Type: String 59 | MinLength: "1" 60 | MaxLength: "16" 61 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 62 | ConstraintDescription: must begin with a letter and contain only alphanumeric characters. 63 | DBPassword: 64 | NoEcho: "true" 65 | Description: The database admin account password 66 | Type: String 67 | MinLength: "8" 68 | MaxLength: "41" 69 | ConstraintDescription: must have at least 8 & at most 41 characters 70 | DBPort: 71 | Description: Database port 72 | Type: Number 73 | Default: 3306 74 | MinValue: 1025 75 | MaxValue: 65535 76 | ConstraintDescription: must be a valid number between 1025-65535 77 | DBInstanceClass: 78 | Description: The database instance type 79 | Type: String 80 | AllowedValues: 81 | - db.r5.large 82 | - db.r5.xlarge 83 | - db.r5.2xlarge 84 | - db.r5.4xlarge 85 | - db.r5.8xlarge 86 | - db.r5.12xlarge 87 | ConstraintDescription: must select a valid database instance type. 88 | DBEngineVersion: 89 | Description: The database engine Version 90 | Type: String 91 | Default: 8.0.mysql_aurora.3.03.1 92 | 93 | Conditions: 94 | IsUnencrypted: !Equals [!Ref DBEncryptionKeyType, Unencrypted] 95 | IsDefaultKey: !Equals [!Ref DBEncryptionKeyType, Default-Key] 96 | IsUserProvidedKey: !Not [!Equals [!Ref EncryptionKeyId, ""]] 97 | IsCreateNewKey: 98 | !And [ 99 | !Not [!Or [Condition: IsUnencrypted, Condition: IsDefaultKey]], 100 | !Equals [!Ref EncryptionKeyId, ""], 101 | ] 102 | 103 | Resources: 104 | DBSecurityGroup: 105 | Type: "AWS::EC2::SecurityGroup" 106 | Properties: 107 | GroupDescription: Open database for access 108 | VpcId: !Ref VpcId 109 | SecurityGroupIngress: 110 | - IpProtocol: tcp 111 | FromPort: !Ref DBPort 112 | ToPort: !Ref DBPort 113 | # Open to all components 114 | CidrIp: 0.0.0.0/0 115 | Tags: 116 | - Key: Application 117 | Value: !Ref "AWS::StackId" 118 | DBSubnetGroup: 119 | Type: "AWS::RDS::DBSubnetGroup" 120 | Properties: 121 | DBSubnetGroupDescription: Subnets the database belongs to 122 | SubnetIds: !Ref "SubnetIDs" 123 | Tags: 124 | - Key: Application 125 | Value: !Ref "AWS::StackId" 126 | 127 | KMSKey: 128 | Type: "AWS::KMS::Key" 129 | Condition: IsCreateNewKey 130 | Properties: 131 | KeyPolicy: 132 | Version: 2012-10-17 133 | Id: !Sub "key-${AWS::StackName}" 134 | Statement: 135 | - Sid: Enable IAM User Permissions 136 | Effect: Allow 137 | Principal: 138 | AWS: !Join ["", ["arn:aws:iam::", !Ref "AWS::AccountId", ":root"]] 139 | Action: "kms:*" 140 | Resource: "*" 141 | 142 | RDSCluster: 143 | Type: AWS::RDS::DBCluster 144 | Properties: 145 | DBClusterIdentifier: !Sub "${AWS::StackName}" 146 | CopyTagsToSnapshot: true 147 | DBSubnetGroupName: !Ref DBSubnetGroup 148 | EnableCloudwatchLogsExports: [error, general, slowquery] 149 | Engine: aurora-mysql 150 | EngineMode: provisioned 151 | EngineVersion: !Ref DBEngineVersion 152 | KmsKeyId: 153 | !If [ 154 | IsCreateNewKey, 155 | !Ref KMSKey, 156 | !If [IsUserProvidedKey, !Ref EncryptionKeyId, !Ref "AWS::NoValue"], 157 | ] 158 | MasterUsername: !Ref DBUser 159 | MasterUserPassword: !Ref DBPassword 160 | Port: !Ref DBPort 161 | StorageEncrypted: !If [IsUnencrypted, "false", "true"] 162 | Tags: 163 | - Key: Application 164 | Value: !Ref "AWS::StackId" 165 | - Key: Name 166 | Value: !Sub "${AWS::StackName}-cluster" 167 | VpcSecurityGroupIds: [!Ref DBSecurityGroup] 168 | DeletionPolicy: Snapshot 169 | 170 | RDSDBInstance1: 171 | Type: "AWS::RDS::DBInstance" 172 | Properties: 173 | DBClusterIdentifier: 174 | Ref: RDSCluster 175 | DBInstanceClass: !Ref DBInstanceClass 176 | DBSubnetGroupName: 177 | Ref: DBSubnetGroup 178 | Engine: aurora-mysql 179 | PubliclyAccessible: "false" 180 | Tags: 181 | - Key: Application 182 | Value: !Ref "AWS::StackId" 183 | - Key: Name 184 | Value: !Sub "${AWS::StackName}-db-instance1" 185 | 186 | RDSDBInstance2: 187 | Type: "AWS::RDS::DBInstance" 188 | Properties: 189 | DBClusterIdentifier: 190 | Ref: RDSCluster 191 | DBInstanceClass: !Ref DBInstanceClass 192 | Engine: aurora-mysql 193 | PubliclyAccessible: "false" 194 | Tags: 195 | - Key: Application 196 | Value: !Ref "AWS::StackId" 197 | - Key: Name 198 | Value: !Sub "${AWS::StackName}-db-instance2" 199 | 200 | Outputs: 201 | DBHostname: 202 | Description: Hostname or IP address of master database 203 | Value: !GetAtt RDSCluster.Endpoint.Address 204 | 205 | DBPort: 206 | Description: Port number for connection to master database 207 | Value: !GetAtt RDSCluster.Endpoint.Port 208 | DBUsername: 209 | Description: Username for master database connection 210 | Value: !Ref "DBUser" 211 | -------------------------------------------------------------------------------- /src/aws/wa_ent_lambda.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | # 6 | AWSTemplateFormatVersion: 2010-09-09 7 | Description: >- 8 | AWS CloudFormation Template that contains AWS Lambda functions used in 9 | WhatsApp Enterprise client templates 10 | 11 | Metadata: 12 | AWS::CloudFormation::Interface: 13 | ParameterGroups: 14 | - Label: 15 | default: "Logging configuration" 16 | Parameters: 17 | - LogRetentionDays 18 | ParameterLabels: 19 | LogRetentionDays: 20 | default: "Number of days to retain lambda logs in CloudWatch" 21 | 22 | Parameters: 23 | LogRetentionDays: 24 | Default: '7' 25 | Description: Number of days to retain logs in CloudWatch 26 | Type: Number 27 | AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653] 28 | ConstraintDescription: must select a valid retention value 29 | 30 | Resources: 31 | StoreParameterLambda: 32 | Type: 'AWS::Lambda::Function' 33 | Properties: 34 | Handler: index.handler 35 | Role: !GetAtt LambdaRole.Arn 36 | MemorySize: 128 37 | Timeout: 60 38 | Runtime: python3.8 39 | Code: 40 | ZipFile: | 41 | import json 42 | import logging 43 | import boto3 44 | import urllib3 45 | from botocore.exceptions import ClientError 46 | 47 | LOG = logging.getLogger() 48 | logging.basicConfig(level=logging.INFO) 49 | LOG.setLevel(logging.INFO) 50 | 51 | SUCCESS = 'SUCCESS' 52 | FAILED = 'FAILED' 53 | 54 | http = urllib3.PoolManager() 55 | 56 | def responseBody(evt, ctx, status, data, echo = False): 57 | if status == FAILED: 58 | return { 59 | 'Status': status, 60 | 'Reason': data['Message'] 61 | } 62 | else: 63 | return { 64 | 'Status': status, 65 | 'Reason': 'completed', 66 | 'PhysicalResourceId': evt['StackId'] or ctx.log_stream_name, 67 | 'StackId': evt['StackId'], 68 | 'RequestId': evt['RequestId'], 69 | 'LogicalResourceId': evt['LogicalResourceId'], 70 | 'NoEcho': not echo, 71 | 'Data': data 72 | } 73 | 74 | def send(evt, ctx, status, data = {}, echo = False): 75 | if 'ResponseURL' in evt: 76 | body = responseBody(evt, ctx, status, data, echo) 77 | jsonBody = json.dumps(body) 78 | 79 | if echo: 80 | LOG.info(f"Response: {jsonBody}") 81 | 82 | headers = { 83 | 'content-type': '', 84 | 'content-length': str(len(jsonBody)) 85 | } 86 | 87 | try: 88 | response = http.request( 89 | 'PUT', 90 | evt['ResponseURL'], 91 | headers=headers, 92 | body=jsonBody 93 | ) 94 | LOG.info(f"Status: {response.status}") 95 | except Exception as exp: 96 | raise Exception(f"send(): {exp}") 97 | else: 98 | LOG.error('send(): ResponseURL not found') 99 | 100 | def myException(evt, ctx, msg): 101 | LOG.warning(msg) 102 | data = { 'Message': msg } 103 | send(evt, ctx, FAILED, data, True) 104 | raise Exception(msg) 105 | 106 | def validate(evt, ctx): 107 | if 'StackId' not in evt or 'ResourceProperties' not in evt: 108 | myException(evt, ctx, 'validate(): Missing StackId or ResourceProperties') 109 | 110 | params = [ 'crypto-arn', 'key', 'value' ] 111 | if not all(p in evt['ResourceProperties'] for p in params): 112 | myException(evt, ctx, 'validate(): Required parameters missing') 113 | 114 | LOG.info(f"Stack ID : {evt['StackId']}") 115 | 116 | def cfn_create(evt, ctx): 117 | store_password(evt, ctx) 118 | send(evt, ctx, SUCCESS) 119 | 120 | def cfn_delete(evt, ctx): 121 | delete_password(evt, ctx) 122 | send(evt, ctx, SUCCESS) 123 | 124 | def store_password(evt, ctx): 125 | try: 126 | props = evt['ResourceProperties'] 127 | cli = boto3.client('ssm') 128 | cli.put_parameter(Name=props['key'], Value=str(props['value']), 129 | Type='SecureString', KeyId=props['crypto-arn'], Overwrite=True) 130 | except Exception as exp: 131 | myException(evt, ctx, f"store_password(): {exp}") 132 | 133 | def delete_password(evt, ctx): 134 | try: 135 | cli = boto3.client('ssm') 136 | cli.delete_parameter(Name=evt['ResourceProperties']['key']) 137 | except ClientError as cExp: 138 | if cExp.response['Error']['Code'] == 'ParameterNotFound': 139 | LOG.info('Parameter does not exist, but continuing execution') 140 | except Exception as exp: 141 | myException(evt, ctx, f"delete_password(): {exp}") 142 | 143 | def handler(evt, ctx): 144 | validate(evt, ctx) 145 | req = evt['RequestType'] 146 | if req == 'Create' or req == 'Update': 147 | cfn_create(evt, ctx) 148 | elif req == 'Delete': 149 | cfn_delete(evt, ctx) 150 | 151 | LambdaLogGroup: 152 | Type: 'AWS::Logs::LogGroup' 153 | DependsOn: StoreParameterLambda 154 | DeletionPolicy: Delete 155 | Properties: 156 | LogGroupName: !Join ['', ['/aws/lambda/', !Ref StoreParameterLambda]] 157 | RetentionInDays: !Ref LogRetentionDays 158 | 159 | LambdaRole: 160 | Type: 'AWS::IAM::Role' 161 | Properties: 162 | AssumeRolePolicyDocument: 163 | Version: '2012-10-17' 164 | Statement: 165 | - Effect: Allow 166 | Principal: 167 | Service: lambda.amazonaws.com 168 | Action: 169 | - 'sts:AssumeRole' 170 | Path: /whatsapp/ 171 | 172 | CFNPolicy: 173 | Type: AWS::IAM::Policy 174 | Properties: 175 | PolicyName: CFNPolicy 176 | PolicyDocument: 177 | Version: '2012-10-17' 178 | Statement: 179 | - Effect: Allow 180 | Action: 181 | - 'cloudformation:DescribeStacks' 182 | Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:*' 183 | Roles: [!Ref 'LambdaRole'] 184 | 185 | LogPolicy: 186 | Type: AWS::IAM::Policy 187 | Properties: 188 | PolicyName: LogPolicy 189 | PolicyDocument: 190 | Version: '2012-10-17' 191 | Statement: 192 | - Effect: Allow 193 | Action: 194 | - 'logs:CreateLogStream' 195 | - 'logs:CreateLogGroup' 196 | - 'logs:PutLogEvents' 197 | Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*' 198 | Roles: [!Ref 'LambdaRole'] 199 | 200 | KMSPolicy: 201 | Type: AWS::IAM::Policy 202 | Properties: 203 | PolicyName: KMSPolicy 204 | PolicyDocument: 205 | Version: '2012-10-17' 206 | Statement: 207 | - Effect: Allow 208 | Action: 209 | - 'kms:Encrypt' 210 | - 'kms:ReEncrypt*' 211 | Resource: !Sub 'arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/*' 212 | Roles: [!Ref 'LambdaRole'] 213 | 214 | SSMPolicy: 215 | Type: AWS::IAM::Policy 216 | Properties: 217 | PolicyName: SSMPolicy 218 | PolicyDocument: 219 | Version: '2012-10-17' 220 | Statement: 221 | - Effect: Allow 222 | Action: 223 | - 'ssm:PutParameter' 224 | - 'ssm:GetParameter' 225 | - 'ssm:DeleteParameter' 226 | Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*' 227 | Roles: [!Ref 'LambdaRole'] 228 | 229 | Outputs: 230 | StoreParameterLambdaArn: 231 | Description: Store Parameter Lambda 232 | Value: !GetAtt StoreParameterLambda.Arn 233 | -------------------------------------------------------------------------------- /src/azure/variables.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | 10 | # General Configuration 11 | variable "name-prefix" { 12 | default = "wabiz" 13 | } 14 | 15 | variable "location" { 16 | default = "eastasia" 17 | } 18 | 19 | variable "owner" { 20 | default = "meta" 21 | } 22 | 23 | variable "ssh-pub-key" { 24 | default = "~/.ssh/azure-aks.pub" 25 | } 26 | 27 | # Throughput Configuration 28 | 29 | variable "throughput" { 30 | type = number 31 | default = 200 32 | 33 | validation { 34 | condition = contains([10, 20, 40, 60, 80, 100, 120,160,200], var.throughput) 35 | error_message = "Valid values var.throughput are: 10, 20, 40, 60, 80, 100, 120, 160, 200." 36 | } 37 | } 38 | 39 | variable "message_type" { 40 | type = string 41 | default = "video" 42 | 43 | validation { 44 | condition = contains(["text", "audio", "video", "doc", "image1MB", "image2MB", "image4MB"], var.message_type) 45 | error_message = "Valid values for var.messageType are: text, audio, video, doc, image1MB, image2MB, image4MB." 46 | } 47 | } 48 | 49 | # WhatsApp Business API Configuration 50 | 51 | variable "api-version" { 52 | default = "v2.41.3" 53 | } 54 | 55 | variable "wabiz-web-username" { 56 | default = "admin" 57 | } 58 | 59 | variable "wabiz-web-password" { 60 | type = string 61 | description = "WhatsApp Business API Password" 62 | validation { 63 | condition = length(var.wabiz-web-password) >= 8 && length(var.wabiz-web-password) <= 64 64 | error_message = "Password needs to be 8-64 characters long with at least 1 digit, 1 uppercase letter, 1 lowercase letter and 1 special character" 65 | } 66 | default = "" 67 | } 68 | 69 | # Database Configuration 70 | 71 | variable "dbusername" { 72 | default = "dbadmin" 73 | } 74 | 75 | variable "dbpassword" { 76 | type = string 77 | description = "Database admin user password" 78 | validation { 79 | condition = length(var.dbpassword) > 0 80 | error_message = "Database admin user password cannot be empty. Should NOT contain any of these characters: ?{}&~!()^=" 81 | } 82 | default = "" 83 | } 84 | 85 | variable "DBCertURL" { 86 | default = "" # mysql in vm 87 | # default = "https://dl.cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem" # flexible server 88 | # default = "https://www.digicert.com/CACerts/BaltimoreCyberTrustRoot.crt.pem" # single server 89 | } 90 | 91 | variable "DBConnCA" { 92 | default = "/opt/certs/db-ca.pem" 93 | } 94 | 95 | # Grafana Configuration 96 | 97 | variable "mon-web-password" { 98 | description = "Set the Grafana dashboard login password" 99 | validation { 100 | condition = length(var.mon-web-password) > 0 101 | error_message = "Grafana admin user password cannot be empty." 102 | } 103 | default = "" 104 | } 105 | 106 | variable "mon-smtp-enabled" { 107 | default = "0" 108 | } 109 | 110 | variable "mon-smtp-host" { 111 | default = "" 112 | } 113 | 114 | variable "mon-smtp-username" { 115 | default = "" 116 | } 117 | 118 | variable "mon-smtp-password" { 119 | default = "" 120 | } 121 | 122 | # Default Configurations - No need to adjust 123 | 124 | #use by k8s management pods 125 | variable "k8s-vm-class" { 126 | default = "Standard_D2s_v4" 127 | } 128 | 129 | # at least 2 for multi-connect required 130 | variable "map_web_server_count" { 131 | type = map(number) 132 | default = { 133 | 10 = 2 134 | 20 = 2 135 | 40 = 2 136 | 60 = 2 137 | 80 = 2 138 | 100 = 2 139 | 120 = 2 140 | 160 = 2 141 | 200 = 2 142 | } 143 | } 144 | 145 | variable "map_shards_count" { 146 | type = map(number) 147 | default = { 148 | 10 = 2 149 | 20 = 4 150 | 40 = 8 151 | 80 = 16 152 | 120 = 32 153 | 160 = 32 154 | 200 = 32 155 | } 156 | } 157 | 158 | variable "map_coreapp_class" { 159 | 160 | default = { 161 | "text" = { 162 | 10 = "Standard_F2s_v2" 163 | 20 = "Standard_F2s_v2" 164 | 40 = "Standard_F2s_v2" 165 | 80 = "Standard_F2s_v2" 166 | 120 = "Standard_F2s_v2" 167 | 160 = "Standard_F2s_v2" 168 | 200 = "Standard_F2s_v2" 169 | }, 170 | "video" = { 171 | 10 = "Standard_F2s_v2" 172 | 20 = "Standard_F2s_v2" 173 | 40 = "Standard_F2s_v2" 174 | 80 = "Standard_F2s_v2" 175 | 120 = "Standard_F2s_v2" 176 | 160 = "Standard_F2s_v2" 177 | 200 = "Standard_F2s_v2" 178 | }, 179 | "audio" = { 180 | 10 = "Standard_F2s_v2" 181 | 20 = "Standard_F2s_v2" 182 | 40 = "Standard_F2s_v2" 183 | 80 = "Standard_F2s_v2" 184 | 120 = "Standard_F2s_v2" 185 | 160 = "Standard_F2s_v2" 186 | 200 = "Standard_F2s_v2" 187 | }, 188 | 189 | "doc" = { 190 | 10 = "Standard_F2s_v2" 191 | 20 = "Standard_F2s_v2" 192 | 40 = "Standard_F2s_v2" 193 | 80 = "Standard_F2s_v2" 194 | 120 = "Standard_F2s_v2" 195 | 160 = "Standard_F2s_v2" 196 | 200 = "Standard_F2s_v2" 197 | }, 198 | 199 | "image1MB" = { 200 | 10 = "Standard_F2s_v2" 201 | 20 = "Standard_F4s_v2" 202 | 40 = "Standard_F4s_v2" 203 | 80 = "Standard_F4s_v2" 204 | 120 = "Standard_F4s_v2" 205 | 160 = "Standard_F4s_v2" 206 | 200 = "Standard_F4s_v2" 207 | }, 208 | 209 | "image2MB" = { 210 | 10 = "Standard_F8s_v2" 211 | 20 = "Standard_F8s_v2" 212 | 40 = "Standard_F8s_v2" 213 | 80 = "Standard_F8s_v2" 214 | 120 = "Standard_F8s_v2" 215 | 160 = "Standard_F8s_v2" 216 | 200 = "Standard_F16s_v2" 217 | }, 218 | 219 | "image4MB" = { 220 | 10 = "Standard_F16s_v2" 221 | 20 = "Standard_F16s_v2" 222 | 40 = "Standard_F16s_v2" 223 | 80 = "Standard_F16s_v2" 224 | 120 = "Standard_F16s_v2" 225 | 160 = "Standard_F16s_v2" 226 | 200 = "Standard_F16s_v2" 227 | }, 228 | } 229 | } 230 | 231 | variable "map_db_class" { 232 | type = map(string) 233 | default = { 234 | 10 = "Standard_E2as_v4" 235 | 20 = "Standard_E2as_v4" 236 | 40 = "Standard_E2as_v4" 237 | 80 = "Standard_E4as_v4" 238 | 120 = "Standard_E8as_v4" 239 | 160 = "Standard_E8as_v4" 240 | 200 = "Standard_E16as_v4" 241 | } 242 | } 243 | variable "map_db_iops" { 244 | type = map(string) 245 | default = { 246 | 10 = 800 247 | 20 = 1500 248 | 40 = 2500 249 | 80 = 3500 250 | 120 = 4000 251 | 160 = 5000 252 | 200 = 6000 253 | } 254 | } 255 | 256 | variable "map_db_throughput" { 257 | type = map(string) 258 | default = { 259 | 10 = 20 260 | 20 = 40 261 | 40 = 60 262 | 80 = 80 263 | 120 = 120 264 | 160 = 150 265 | 200 = 180 266 | } 267 | } 268 | 269 | /* do NOT uncomment below code 270 | 271 | variable "map_db_buffer_pool_size" { 272 | type = map(string) 273 | default = { 274 | 10 = 1024 * 1024 * 1024 * 16 * 0.75 275 | 20 = 1024 * 1024 * 1024 * 16 * 0.75 276 | 40 = 1024 * 1024 * 1024 * 32 * 0.75 277 | 80 = 1024 * 1024 * 1024 * 32 * 0.75 278 | 120 = 1024 * 1024 * 1024 * 64 * 0.75 279 | 160 = 1024 * 1024 * 1024 * 64 * 0.75 280 | 200 = 1024 * 1024 * 1024 * 256 * 0.75 281 | 250 = 1024 * 1024 * 1024 * 256 * 0.75 282 | 300 = 1024 * 1024 * 1024 * 256 * 0.75 283 | } 284 | } 285 | 286 | variable "map_db_buffer_pool_instances" { 287 | type = map(string) 288 | default = { 289 | 10 = 2 290 | 20 = 2 291 | 40 = 4 292 | 80 = 4 293 | 120 = 8 294 | 160 = 8 295 | 200 = 32 296 | 250 = 32 297 | 300 = 32 298 | } 299 | } 300 | 301 | variable "map_db_query_cache_size" { 302 | type = map(string) 303 | default = { 304 | 10 = 1024 * 1024 * 1024 * 2 305 | 20 = 1024 * 1024 * 1024 * 2 306 | 40 = 1024 * 1024 * 1024 * 4 307 | 80 = 1024 * 1024 * 1024 * 4 308 | 120 = 1024 * 1024 * 1024 * 8 309 | 160 = 1024 * 1024 * 1024 * 8 310 | 200 = 1024 * 1024 * 1024 * 32 311 | 250 = 1024 * 1024 * 1024 * 32 312 | 300 = 1024 * 1024 * 1024 * 32 313 | } 314 | } 315 | 316 | variable "map_db_size" { 317 | type = map(number) 318 | default = { 319 | 10 = 1024 * 64 #64GB / 64 x 3 = 192 IOPS 320 | 20 = 1024 * 128 321 | 30 = 1024 * 256 322 | 40 = 1024 * 256 323 | 60 = 1024 * 512 324 | 80 = 1024 * 512 325 | 100 = 1024 * 512 326 | 120 = 1024 * 512 327 | 160 = 1024 * 1024 328 | 200 = 1024 * 1024 329 | } 330 | } 331 | 332 | variable "map_db_class" { 333 | type = map(string) 334 | default = { 335 | 10 = "GP_Gen5_2" 336 | 20 = "GP_Gen5_2" 337 | 30 = "GP_Gen5_2" 338 | 40 = "GP_Gen5_2" 339 | 60 = "GP_Gen5_2" 340 | 80 = "GP_Gen5_2" 341 | 100 = "GP_Gen5_2" 342 | 120 = "GP_Gen5_2" 343 | 160 = "GP_Gen5_4" 344 | 200 = "GP_Gen5_4" 345 | } 346 | } 347 | 348 | 349 | */ 350 | -------------------------------------------------------------------------------- /src/gcp/k8s-waent.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | 11 | locals { 12 | root-vol = "root-vol" 13 | root-vol-src = "/" 14 | root-vol-container-path = "/rootfs" 15 | tmpfs-vol = "tmpfs" 16 | tmpfs-vol-src = "/var/run" 17 | tmpfs-vol-container-path = "/var/run" 18 | sys-vol = "sys-vol" 19 | sys-vol-src = "/sys" 20 | sys-vol-container-path = "/host/sys" 21 | docker-vol = "docker-vol" 22 | docker-vol-src = "/var/lib/docker" 23 | docker-vol-container-path = "/var/lib/docker" 24 | device-vol = "device-vol" 25 | device-vol-src = "/dev/disk" 26 | device-vol-container-path = "/dev/disk" 27 | proc-vol = "proc-vol" 28 | proc-vol-src = "/proc" 29 | proc-vol-container-path = "/host/proc" 30 | } 31 | 32 | locals { 33 | prom-vol = "prom-vol" 34 | prom-vol-src = "/prometheus-data" 35 | prom-vol-container-path = "/prometheus-data" 36 | grafana-vol = "grafana-vol" 37 | grafana-vol-src = "/var/lib/grafana" 38 | grafana-vol-container-path = "/var/lib/grafana" 39 | } 40 | 41 | 42 | locals { 43 | number_of_masterapp = 2 44 | mysql_credential_mount_path = "/var/mysql/credential" 45 | mysql_init_vol = "mysql-init-vol" 46 | mysql_init_mount_path = "/var/mysql/init" 47 | db_vol = "db-vol" 48 | db_mount_path = "/var/lib/mysql" 49 | db_sub_path = "mysql" 50 | db_config_mount_path = "/etc/mysql" 51 | db_config_sub_path = "etc/mysql" 52 | media_mount_path = "/usr/local/wamedia" 53 | media_vol = "media-vol" 54 | media_sub_path = "waent/media" 55 | data_mount_path = "/usr/local/waent/data" 56 | data_sub_path = "waent/data" 57 | config_map_ref_name = "config-env" 58 | config_map_ref_name_master = "config-master" 59 | secret_map_ref_name = "secret-env" 60 | init_cmd = "export WA_DB_SSL_CA= && cd /opt/whatsapp/bin && ./launch_within_docker.sh" #DB in VM 61 | init_cmd_coreapp = "export WA_DB_SSL_CA= && cd /opt/whatsapp/bin && IP=$(hostname -I | awk '{print $1}') && export COREAPP_HOSTNAME=$IP && ./launch_within_docker.sh" 62 | } 63 | 64 | resource "kubernetes_deployment" "webapp" { 65 | count = var.nfs-pvc-creation-complete ? 1 : 0 66 | 67 | timeouts { 68 | create = "5m" 69 | update = "2m" 70 | delete = "1m" 71 | } 72 | metadata { 73 | name = "webapp" 74 | labels = { 75 | type = "webapp" 76 | } 77 | } 78 | spec { 79 | replicas = var.map_web_server_count[var.throughput] 80 | selector { 81 | match_labels = { 82 | type = "webapp" 83 | } 84 | } 85 | 86 | template { 87 | metadata { 88 | name = "webapp" 89 | labels = { type = "webapp" } 90 | } 91 | spec { 92 | affinity { 93 | pod_anti_affinity { 94 | required_during_scheduling_ignored_during_execution { 95 | label_selector { 96 | match_expressions { 97 | key = "type" 98 | operator = "In" 99 | values = ["webapp"] 100 | } 101 | } 102 | topology_key = "kubernetes.io/hostname" 103 | } 104 | } 105 | 106 | node_affinity { 107 | required_during_scheduling_ignored_during_execution { 108 | node_selector_term { 109 | match_expressions { 110 | key = "type" 111 | operator = "In" 112 | values = ["webapp"] 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | container { 120 | image = "docker.whatsapp.biz/web:${var.api-version}" 121 | name = "webapp" 122 | 123 | command = ["/bin/sh", "-c"] 124 | 125 | args = [ 126 | local.init_cmd 127 | ] 128 | 129 | volume_mount { 130 | name = local.media_vol 131 | mount_path = local.media_mount_path 132 | sub_path = local.media_sub_path 133 | } 134 | 135 | volume_mount { 136 | name = local.media_vol 137 | mount_path = local.data_mount_path 138 | sub_path = local.data_sub_path 139 | } 140 | 141 | env_from { 142 | config_map_ref { 143 | name = kubernetes_config_map.env.metadata.0.name 144 | } 145 | } 146 | 147 | env_from { 148 | secret_ref { 149 | name = kubernetes_secret.env.metadata.0.name 150 | } 151 | } 152 | 153 | port { 154 | container_port = 443 155 | } 156 | } 157 | 158 | volume { 159 | name = local.media_vol 160 | persistent_volume_claim { 161 | # manifest embedded yaml will also fail 162 | # claim_name = "nfs-pvc-test" 163 | 164 | #only raw yaml works 165 | claim_name = "nfs-pvc" 166 | } 167 | } 168 | } 169 | } 170 | 171 | strategy { 172 | type = "RollingUpdate" 173 | 174 | rolling_update { 175 | max_unavailable = "1" 176 | max_surge = "1" 177 | } 178 | } 179 | revision_history_limit = 10 180 | } 181 | depends_on = [google_container_node_pool.webapp, kubernetes_service.nfs_server] 182 | } 183 | 184 | resource "kubernetes_deployment" "coreapp" { 185 | count = var.nfs-pvc-creation-complete ? 1 : 0 186 | 187 | timeouts { 188 | create = "10m" 189 | update = "5m" 190 | delete = "1m" 191 | } 192 | metadata { 193 | name = "coreapp" 194 | labels = { 195 | type = "coreapp" 196 | } 197 | } 198 | 199 | spec { 200 | replicas = var.map_shards_count[var.throughput] + 1 // one more for disconnected HA coreapp 201 | selector { 202 | match_labels = { 203 | type = "coreapp" 204 | } 205 | } 206 | 207 | template { 208 | metadata { 209 | name = "coreapp" 210 | labels = { type = "coreapp" } 211 | } 212 | 213 | spec { 214 | affinity { 215 | pod_anti_affinity { 216 | required_during_scheduling_ignored_during_execution { 217 | label_selector { 218 | match_expressions { 219 | key = "type" 220 | operator = "In" 221 | values = ["coreapp"] 222 | } 223 | } 224 | topology_key = "kubernetes.io/hostname" 225 | } 226 | } 227 | node_affinity { 228 | required_during_scheduling_ignored_during_execution { 229 | node_selector_term { 230 | match_expressions { 231 | key = "type" 232 | operator = "In" 233 | values = ["coreapp"] 234 | } 235 | } 236 | } 237 | } 238 | } 239 | 240 | volume { 241 | name = local.media_vol 242 | persistent_volume_claim { 243 | # manifest embedded yaml will fail 244 | # claim_name = "nfs-pvc-test" 245 | 246 | #raw yaml works 247 | claim_name = "nfs-pvc" 248 | } 249 | } 250 | 251 | container { 252 | image = "docker.whatsapp.biz/coreapp:${var.api-version}" 253 | name = "coreapp" 254 | 255 | command = ["/bin/sh", "-c"] 256 | 257 | args = [ 258 | local.init_cmd_coreapp 259 | ] 260 | 261 | volume_mount { 262 | name = local.media_vol 263 | mount_path = local.media_mount_path 264 | sub_path = local.media_sub_path 265 | } 266 | 267 | volume_mount { 268 | name = local.media_vol 269 | mount_path = local.data_mount_path 270 | sub_path = local.data_sub_path 271 | } 272 | 273 | env_from { 274 | config_map_ref { 275 | name = local.config_map_ref_name 276 | } 277 | } 278 | 279 | env_from { 280 | secret_ref { 281 | name = local.secret_map_ref_name 282 | } 283 | } 284 | 285 | port { 286 | container_port = 6250 287 | } 288 | port { 289 | container_port = 6251 290 | } 291 | port { 292 | container_port = 6252 293 | } 294 | port { 295 | container_port = 6253 296 | } 297 | } 298 | } 299 | } 300 | 301 | strategy { 302 | type = "RollingUpdate" 303 | 304 | rolling_update { 305 | max_unavailable = "1" 306 | max_surge = "1" 307 | } 308 | } 309 | revision_history_limit = 10 310 | } 311 | 312 | depends_on = [google_container_node_pool.coreapp, kubernetes_service.nfs_server] 313 | } 314 | 315 | resource "kubernetes_deployment" "masterapp" { 316 | count = var.nfs-pvc-creation-complete ? 1 : 0 317 | 318 | timeouts { 319 | create = "5m" 320 | update = "2m" 321 | delete = "1m" 322 | } 323 | metadata { 324 | name = "masterapp" 325 | labels = { 326 | type = "masterapp" 327 | } 328 | } 329 | 330 | spec { 331 | replicas = local.number_of_masterapp 332 | selector { 333 | match_labels = { 334 | type = "masterapp" 335 | } 336 | } 337 | 338 | template { 339 | metadata { 340 | name = "masterapp" 341 | labels = { type = "masterapp" } 342 | } 343 | 344 | spec { 345 | affinity { 346 | pod_anti_affinity { 347 | required_during_scheduling_ignored_during_execution { 348 | label_selector { 349 | match_expressions { 350 | key = "type" 351 | operator = "In" 352 | values = ["masterapp"] 353 | } 354 | } 355 | topology_key = "kubernetes.io/hostname" 356 | } 357 | } 358 | node_affinity { 359 | required_during_scheduling_ignored_during_execution { 360 | node_selector_term { 361 | match_expressions { 362 | key = "type" 363 | operator = "In" 364 | values = ["webapp"] 365 | } 366 | } 367 | } 368 | } 369 | } 370 | 371 | container { 372 | image = "docker.whatsapp.biz/coreapp:${var.api-version}" 373 | name = "masterapp" 374 | 375 | command = ["/bin/sh", "-c"] 376 | 377 | args = [ 378 | local.init_cmd_coreapp 379 | ] 380 | 381 | env_from { 382 | config_map_ref { 383 | name = local.config_map_ref_name 384 | } 385 | } 386 | 387 | env_from { 388 | config_map_ref { 389 | name = local.config_map_ref_name_master 390 | } 391 | } 392 | 393 | env_from { 394 | secret_ref { 395 | name = local.secret_map_ref_name 396 | } 397 | } 398 | 399 | port { 400 | container_port = 6250 401 | } 402 | port { 403 | container_port = 6251 404 | } 405 | port { 406 | container_port = 6252 407 | } 408 | port { 409 | container_port = 6253 410 | } 411 | } 412 | 413 | } 414 | } 415 | 416 | strategy { 417 | type = "RollingUpdate" 418 | 419 | rolling_update { 420 | max_unavailable = "1" 421 | max_surge = "1" 422 | } 423 | } 424 | revision_history_limit = 10 425 | } 426 | depends_on = [google_container_node_pool.coreapp] 427 | } 428 | 429 | 430 | resource "kubernetes_service" "webapp" { 431 | count = var.nfs-pvc-creation-complete ? 1 : 0 432 | 433 | timeouts { 434 | create = "2m" 435 | } 436 | metadata { 437 | name = "webapp" 438 | labels = { type = "webapp" } 439 | } 440 | spec { 441 | selector = { 442 | type = "webapp" 443 | } 444 | 445 | port { 446 | port = 443 447 | target_port = 443 448 | } 449 | 450 | type = "LoadBalancer" 451 | } 452 | depends_on = [kubernetes_deployment.webapp] 453 | } 454 | -------------------------------------------------------------------------------- /src/gcp/k8s-monitor.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API GCP Template Version 1.0.0 9 | 10 | resource "google_container_node_pool" "monitor" { 11 | name = "monitor-node-pool" 12 | location = var.zone 13 | cluster = google_container_cluster.waApi.name 14 | node_count = 2 15 | node_config { 16 | preemptible = false 17 | machine_type = "n2-highcpu-2" 18 | oauth_scopes = [ 19 | "https://www.googleapis.com/auth/cloud-platform", 20 | ] 21 | 22 | labels = { 23 | type = "monitor" 24 | } 25 | tags = [var.owner] 26 | 27 | metadata = { 28 | disable-legacy-endpoints = "true" 29 | } 30 | } 31 | } 32 | 33 | resource "kubernetes_persistent_volume_claim" "prometheus" { 34 | count = var.nfs-pvc-creation-complete ? 1 : 0 35 | timeouts { 36 | create = "5m" 37 | 38 | } 39 | metadata { 40 | name = "prometheus" 41 | } 42 | spec { 43 | access_modes = ["ReadWriteOnce"] 44 | resources { 45 | requests = { 46 | storage = "10Gi" 47 | } 48 | } 49 | storage_class_name = kubernetes_storage_class.prometheus.metadata.0.name 50 | } 51 | 52 | depends_on = [kubernetes_storage_class.prometheus] 53 | } 54 | 55 | resource "kubernetes_storage_class" "prometheus" { 56 | metadata { 57 | name = "prometheus" 58 | } 59 | 60 | storage_provisioner = "pd.csi.storage.gke.io" 61 | volume_binding_mode = "Immediate" 62 | allow_volume_expansion = true 63 | 64 | parameters = { 65 | type = "pd-standard" 66 | } 67 | } 68 | 69 | resource "kubernetes_persistent_volume_claim" "grafana" { 70 | count = var.nfs-pvc-creation-complete ? 1 : 0 71 | timeouts { 72 | 73 | create = "5m" 74 | 75 | } 76 | metadata { 77 | name = "grafana" 78 | } 79 | spec { 80 | access_modes = ["ReadWriteOnce"] 81 | resources { 82 | requests = { 83 | storage = "10Gi" 84 | } 85 | } 86 | storage_class_name = kubernetes_storage_class.grafana.metadata.0.name 87 | } 88 | 89 | depends_on = [kubernetes_storage_class.grafana] 90 | } 91 | 92 | resource "kubernetes_storage_class" "grafana" { 93 | metadata { 94 | name = "grafana" 95 | } 96 | 97 | storage_provisioner = "pd.csi.storage.gke.io" 98 | volume_binding_mode = "Immediate" 99 | allow_volume_expansion = true 100 | 101 | parameters = { 102 | type = "pd-standard" 103 | } 104 | } 105 | 106 | resource "kubernetes_deployment" "monitor" { 107 | count = var.nfs-pvc-creation-complete ? 1 : 0 108 | depends_on = [google_container_node_pool.monitor] 109 | timeouts { 110 | create = "10m" 111 | update = "5m" 112 | delete = "1m" 113 | } 114 | metadata { 115 | name = "monitor" 116 | labels = { 117 | type = "monitor" 118 | } 119 | } 120 | spec { 121 | replicas = 1 122 | selector { 123 | match_labels = { 124 | type = "monitor" 125 | } 126 | } 127 | 128 | template { 129 | metadata { 130 | name = "monitor" 131 | labels = { type = "monitor" } 132 | } 133 | 134 | spec { 135 | affinity { 136 | pod_anti_affinity { 137 | required_during_scheduling_ignored_during_execution { 138 | label_selector { 139 | match_expressions { 140 | key = "type" 141 | operator = "In" 142 | values = ["monitor"] 143 | } 144 | } 145 | topology_key = "kubernetes.io/hostname" 146 | } 147 | } 148 | node_affinity { 149 | required_during_scheduling_ignored_during_execution { 150 | node_selector_term { 151 | match_expressions { 152 | key = "type" 153 | operator = "In" 154 | values = ["monitor"] 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | volume { 162 | name = local.prom-vol 163 | persistent_volume_claim { 164 | claim_name = kubernetes_persistent_volume_claim.prometheus.0.metadata.0.name 165 | } 166 | } 167 | 168 | volume { 169 | name = local.grafana-vol 170 | persistent_volume_claim { 171 | claim_name = kubernetes_persistent_volume_claim.grafana.0.metadata.0.name 172 | } 173 | } 174 | 175 | container { 176 | image = "prom/mysqld-exporter:v0.10.0" 177 | name = "mysqld-exporter" 178 | 179 | resources { 180 | requests = { 181 | memory = "100M" 182 | } 183 | } 184 | 185 | env_from { 186 | secret_ref { 187 | name = kubernetes_secret.db.metadata.0.name 188 | } 189 | } 190 | 191 | port { 192 | container_port = 9104 193 | } 194 | } 195 | 196 | container { 197 | image = "docker.whatsapp.biz/prometheus:${var.api-version}" 198 | name = "prometheus" 199 | 200 | security_context { 201 | run_as_user = 0 202 | } 203 | 204 | volume_mount { 205 | name = local.prom-vol 206 | mount_path = local.prom-vol-container-path 207 | } 208 | 209 | resources { 210 | requests = { 211 | memory = "100Mi" 212 | } 213 | } 214 | 215 | env_from { 216 | config_map_ref { 217 | name = kubernetes_config_map.mon-prom[0].metadata.0.name 218 | } 219 | } 220 | 221 | env_from { 222 | secret_ref { 223 | name = kubernetes_secret.mon-prom.metadata.0.name 224 | } 225 | } 226 | 227 | port { 228 | container_port = 9090 229 | } 230 | } 231 | 232 | 233 | container { 234 | image = "docker.whatsapp.biz/grafana:${var.api-version}" 235 | name = "grafana" 236 | 237 | volume_mount { 238 | name = local.grafana-vol 239 | mount_path = local.grafana-vol-container-path 240 | } 241 | 242 | resources { 243 | requests = { 244 | memory = "100Mi" 245 | } 246 | } 247 | 248 | env_from { 249 | config_map_ref { 250 | name = kubernetes_config_map.mon-graf.metadata.0.name 251 | } 252 | } 253 | 254 | env_from { 255 | secret_ref { 256 | name = kubernetes_secret.mon-graf.metadata.0.name 257 | } 258 | } 259 | 260 | port { 261 | container_port = 3000 262 | } 263 | } 264 | 265 | } 266 | } 267 | 268 | strategy { 269 | type = "RollingUpdate" 270 | 271 | rolling_update { 272 | max_unavailable = "1" 273 | max_surge = "1" 274 | } 275 | } 276 | revision_history_limit = 10 277 | } 278 | } 279 | 280 | resource "kubernetes_daemonset" "exporter" { 281 | count = var.nfs-pvc-creation-complete ? 1 : 0 282 | metadata { 283 | name = "exporter" 284 | labels = { 285 | type = "exporter" 286 | } 287 | } 288 | 289 | spec { 290 | selector { 291 | match_labels = { 292 | type = "exporter" 293 | } 294 | } 295 | 296 | template { 297 | metadata { 298 | labels = { 299 | type = "exporter" 300 | } 301 | } 302 | 303 | spec { 304 | affinity { 305 | node_affinity { 306 | required_during_scheduling_ignored_during_execution { 307 | node_selector_term { 308 | match_expressions { 309 | key = "type" 310 | operator = "In" 311 | values = ["coreapp", "webapp"] 312 | } 313 | } 314 | } 315 | } 316 | } 317 | 318 | volume { 319 | name = local.root-vol 320 | host_path { 321 | path = local.root-vol-src 322 | } 323 | } 324 | 325 | volume { 326 | name = local.tmpfs-vol 327 | host_path { 328 | path = local.tmpfs-vol-src 329 | } 330 | } 331 | 332 | volume { 333 | name = local.sys-vol 334 | host_path { 335 | path = local.sys-vol-src 336 | } 337 | } 338 | 339 | volume { 340 | name = local.docker-vol 341 | host_path { 342 | path = local.docker-vol-src 343 | } 344 | } 345 | 346 | volume { 347 | name = local.device-vol 348 | host_path { 349 | path = local.device-vol-src 350 | } 351 | } 352 | 353 | volume { 354 | name = local.proc-vol 355 | host_path { 356 | path = local.proc-vol-src 357 | } 358 | } 359 | 360 | container { 361 | image = "google/cadvisor:v0.30.2" 362 | name = "cadvisor" 363 | 364 | volume_mount { 365 | name = local.root-vol 366 | mount_path = local.root-vol-container-path 367 | } 368 | 369 | volume_mount { 370 | name = local.sys-vol 371 | mount_path = local.sys-vol-container-path 372 | } 373 | 374 | volume_mount { 375 | name = local.docker-vol 376 | mount_path = local.docker-vol-container-path 377 | } 378 | 379 | volume_mount { 380 | name = local.device-vol 381 | mount_path = local.device-vol-container-path 382 | } 383 | 384 | volume_mount { 385 | name = local.tmpfs-vol 386 | mount_path = local.tmpfs-vol-container-path 387 | } 388 | 389 | resources { 390 | requests = { 391 | memory = "128Mi" 392 | } 393 | } 394 | 395 | port { 396 | container_port = 8080 397 | } 398 | } 399 | 400 | container { 401 | image = "prom/node-exporter:v0.16.0" 402 | name = "node-exporter" 403 | 404 | volume_mount { 405 | name = local.root-vol 406 | mount_path = local.root-vol-container-path 407 | read_only = true 408 | } 409 | 410 | volume_mount { 411 | name = local.sys-vol 412 | mount_path = local.sys-vol-container-path 413 | read_only = true 414 | } 415 | 416 | volume_mount { 417 | name = local.proc-vol 418 | mount_path = local.proc-vol-container-path 419 | read_only = true 420 | } 421 | 422 | resources { 423 | requests = { 424 | memory = "32Mi" 425 | } 426 | } 427 | 428 | port { 429 | container_port = 9100 430 | } 431 | } 432 | 433 | } 434 | } 435 | } 436 | } 437 | 438 | 439 | resource "kubernetes_service" "exporter-coreapp" { 440 | count = var.nfs-pvc-creation-complete ? 1 : 0 441 | metadata { 442 | name = "exporter-coreapp" 443 | labels = { type = "monitor" } 444 | } 445 | spec { 446 | selector = { 447 | type = "coreapp" 448 | } 449 | 450 | port { 451 | name = "cadvisor" 452 | port = 8080 453 | target_port = 8080 454 | } 455 | 456 | port { 457 | name = "node-exporter" 458 | port = 9100 459 | target_port = 9100 460 | } 461 | } 462 | depends_on = [kubernetes_deployment.monitor] 463 | } 464 | 465 | resource "kubernetes_service" "exporter-webapp" { 466 | count = var.nfs-pvc-creation-complete ? 1 : 0 467 | metadata { 468 | name = "exporter-webapp" 469 | labels = { type = "monitor" } 470 | } 471 | spec { 472 | selector = { 473 | type = "webapp" 474 | } 475 | 476 | port { 477 | name = "cadvisor" 478 | port = 8080 479 | target_port = 8080 480 | } 481 | 482 | port { 483 | name = "node-exporter" 484 | port = 9100 485 | target_port = 9100 486 | } 487 | } 488 | depends_on = [kubernetes_deployment.monitor] 489 | } 490 | 491 | resource "kubernetes_service" "mysqld-exporter" { 492 | count = var.nfs-pvc-creation-complete ? 1 : 0 493 | metadata { 494 | name = "mysqld-exporter" 495 | labels = { type = "monitor" } 496 | } 497 | spec { 498 | selector = { 499 | type = "monitor" 500 | } 501 | 502 | port { 503 | name = "mysqld-exporter" 504 | port = 9104 505 | target_port = 9104 506 | } 507 | } 508 | depends_on = [kubernetes_deployment.monitor] 509 | } 510 | 511 | resource "kubernetes_service" "prometheus" { 512 | count = var.nfs-pvc-creation-complete ? 1 : 0 513 | metadata { 514 | name = "prometheus" 515 | labels = { type = "monitor" } 516 | } 517 | spec { 518 | selector = { 519 | type = "monitor" 520 | } 521 | 522 | port { 523 | name = "prometheus" 524 | port = 9090 525 | target_port = 9090 526 | } 527 | } 528 | depends_on = [kubernetes_deployment.monitor] 529 | } 530 | 531 | resource "kubernetes_service" "monitor" { 532 | count = var.nfs-pvc-creation-complete ? 1 : 0 533 | timeouts { 534 | create = "10m" 535 | } 536 | metadata { 537 | name = "monitor" 538 | labels = { type = "monitor" } 539 | } 540 | spec { 541 | selector = { 542 | type = "monitor" 543 | } 544 | 545 | port { 546 | name = "grafana" 547 | port = 3000 548 | target_port = 3000 549 | } 550 | 551 | port { 552 | name = "prometheus" 553 | port = 9090 554 | target_port = 9090 555 | } 556 | 557 | type = "LoadBalancer" 558 | } 559 | 560 | depends_on = [kubernetes_deployment.monitor] 561 | } 562 | -------------------------------------------------------------------------------- /src/aws/wa_ent_net.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | # 6 | AWSTemplateFormatVersion: 2010-09-09 7 | Description: >- 8 | AWS CloudFormation Template to create network. It's believed that network 9 | typically exists for most of the customers using AWS and the same could be 10 | reused. This template is for reference purpose for customers who want to 11 | create VPC & subnets. 12 | Metadata: 13 | AWS::CloudFormation::Interface: 14 | ParameterGroups: 15 | - Label: 16 | default: "Availability Zones configuration" 17 | Parameters: 18 | - AZs 19 | - NumAZs 20 | - Label: 21 | default: "VPC configuration" 22 | Parameters: 23 | - VPCCidr 24 | - VPCTenancy 25 | - Label: 26 | default: "Public Subnet configuration" 27 | Parameters: 28 | - PublicSubnet1Cidr 29 | - PublicSubnet2Cidr 30 | - PublicSubnet3Cidr 31 | - PublicSubnet4Cidr 32 | - Label: 33 | default: "Private subnet configuration" 34 | Parameters: 35 | - CreatePrivateSubnets 36 | - PrivateSubnet1Cidr 37 | - PrivateSubnet2Cidr 38 | - PrivateSubnet3Cidr 39 | - PrivateSubnet4Cidr 40 | ParameterLabels: 41 | AZs: 42 | default: "Availability Zones" 43 | NumAZs: 44 | default: "Number of Availability Zones" 45 | VPCCidr: 46 | default: "Enter IP address range for VPC" 47 | VPCTenancy: 48 | default: "VPC Tenancy" 49 | PublicSubnet1Cidr: 50 | default: "Public subnet #1 IP address range" 51 | PublicSubnet2Cidr: 52 | default: "Public subnet #2 IP address range" 53 | PublicSubnet3Cidr: 54 | default: "Public subnet #3 IP address range" 55 | PublicSubnet4Cidr: 56 | default: "Public subnet #4 IP address range" 57 | CreatePrivateSubnets: 58 | default: "Should private subnets be created in this VPC?" 59 | PrivateSubnet1Cidr: 60 | default: "Private subnet #1 IP address range" 61 | PrivateSubnet2Cidr: 62 | default: "Private subnet #2 IP address range" 63 | PrivateSubnet3Cidr: 64 | default: "Private subnet #3 IP address range" 65 | PrivateSubnet4Cidr: 66 | default: "Private subnet #4 IP address range" 67 | Parameters: 68 | AZs: 69 | Description: >- 70 | List of AZs to use for the subnets in the VPC. 71 | Note: The logical order is preserved. 72 | Type: List 73 | NumAZs: 74 | AllowedValues: [2, 3, 4] 75 | Description: >- 76 | Number of AZs to use in the VPC. This must match your 77 | selections in the list of AZs parameter. 78 | Default: 2 79 | Type: Number 80 | VPCCidr: 81 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 82 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 83 | Default: 10.0.0.0/16 84 | Description: The IP address range for this VPC 85 | MaxLength: '18' 86 | MinLength: '9' 87 | Type: String 88 | VPCTenancy: 89 | AllowedValues: [default, dedicated] 90 | Default: default 91 | Description: The allowed tenancy of instances launched into the VPC 92 | Type: String 93 | PublicSubnet1Cidr: 94 | Description: >- 95 | The IP address range for 'public' subnet in AZ 1 96 | Type: String 97 | MinLength: '9' 98 | MaxLength: '18' 99 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 100 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 101 | Default: 10.0.128.0/20 102 | PublicSubnet2Cidr: 103 | Description: >- 104 | The IP address range for 'public' subnet in AZ 2 105 | Type: String 106 | MinLength: '9' 107 | MaxLength: '18' 108 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 109 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 110 | Default: 10.0.144.0/20 111 | PublicSubnet3Cidr: 112 | Description: >- 113 | The IP address range for 'public' subnet in AZ 3 114 | Type: String 115 | MinLength: '9' 116 | MaxLength: '18' 117 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 118 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 119 | Default: 10.0.160.0/20 120 | PublicSubnet4Cidr: 121 | Description: >- 122 | The IP address range for 'public' subnet in AZ 4 123 | Type: String 124 | MinLength: '9' 125 | MaxLength: '18' 126 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 127 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 128 | Default: 10.0.176.0/20 129 | CreatePrivateSubnets: 130 | Description: >- 131 | Create private subnets in the VPC? 132 | Type: String 133 | Default: true 134 | AllowedValues: [true, false] 135 | PrivateSubnet1Cidr: 136 | Description: >- 137 | The IP address range for 'private' subnet in AZ 1 138 | Type: String 139 | MinLength: '9' 140 | MaxLength: '18' 141 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 142 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 143 | Default: 10.0.0.0/19 144 | PrivateSubnet2Cidr: 145 | Description: >- 146 | The IP address range for 'private' subnet in AZ 2 147 | Type: String 148 | MinLength: '9' 149 | MaxLength: '18' 150 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 151 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 152 | Default: 10.0.32.0/19 153 | PrivateSubnet3Cidr: 154 | Description: >- 155 | The IP address range for 'private' subnet in AZ 3 156 | Type: String 157 | MinLength: '9' 158 | MaxLength: '18' 159 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 160 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 161 | Default: 10.0.64.0/19 162 | PrivateSubnet4Cidr: 163 | Description: >- 164 | The IP address range for 'private' subnet in AZ 4 165 | Type: String 166 | MinLength: '9' 167 | MaxLength: '18' 168 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 169 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 170 | Default: 10.0.96.0/19 171 | Conditions: 172 | Cond3AZ: !Or [!Equals [!Ref NumAZs, 3], !Condition Cond4AZ] 173 | Cond4AZ: !Equals [!Ref NumAZs, 4] 174 | CondPrivateSubnet: !Equals [!Ref CreatePrivateSubnets, true] 175 | CondPrivateSubnet&3AZ: !And [!Equals [!Ref CreatePrivateSubnets, true], !Condition Cond3AZ] 176 | CondPrivateSubnet&4AZ: !And [!Equals [!Ref CreatePrivateSubnets, true], !Condition Cond4AZ] 177 | CondNATGateway: !Condition CondPrivateSubnet 178 | CondNATGateway&3AZ: !And [!Condition CondPrivateSubnet, !Condition Cond3AZ] 179 | CondNATGateway&4AZ: !And [!Condition CondPrivateSubnet, !Condition Cond4AZ] 180 | CondNVirginiaRegion: !Equals [!Ref 'AWS::Region', 'us-east-1'] 181 | Resources: 182 | DHCPOptions: 183 | Type: 'AWS::EC2::DHCPOptions' 184 | Properties: 185 | DomainName: !If 186 | - CondNVirginiaRegion 187 | - ec2.internal 188 | - !Join 189 | - '' 190 | - - !Ref 'AWS::Region' 191 | - .compute.internal 192 | DomainNameServers: 193 | - AmazonProvidedDNS 194 | VPC: 195 | Type: 'AWS::EC2::VPC' 196 | Properties: 197 | CidrBlock: !Ref VPCCidr 198 | EnableDnsSupport: true 199 | EnableDnsHostnames: true 200 | InstanceTenancy: !Ref VPCTenancy 201 | Tags: 202 | - Key: Application 203 | Value: !Ref 'AWS::StackId' 204 | - Key: Name 205 | Value: !Sub 'vpc-${AWS::StackName}' 206 | VPCDHCPOptionsAssociation: 207 | Type: 'AWS::EC2::VPCDHCPOptionsAssociation' 208 | Properties: 209 | VpcId: !Ref VPC 210 | DhcpOptionsId: !Ref DHCPOptions 211 | InternetGateway: 212 | Type: 'AWS::EC2::InternetGateway' 213 | Properties: 214 | Tags: 215 | - Key: Application 216 | Value: !Ref 'AWS::StackId' 217 | - Key: Name 218 | Value: !Ref 'AWS::StackName' 219 | AttachGateway: 220 | Type: 'AWS::EC2::VPCGatewayAttachment' 221 | Properties: 222 | VpcId: !Ref VPC 223 | InternetGatewayId: !Ref InternetGateway 224 | PrivateSubnet1: 225 | Condition: CondPrivateSubnet 226 | Type: 'AWS::EC2::Subnet' 227 | Properties: 228 | AvailabilityZone: !Select [0, !Ref AZs] 229 | CidrBlock: !Ref PrivateSubnet1Cidr 230 | MapPublicIpOnLaunch: false 231 | VpcId: !Ref VPC 232 | Tags: 233 | - Key: Application 234 | Value: !Ref 'AWS::StackId' 235 | - Key: Name 236 | Value: !Sub 'Private subnet 1 (${AWS::StackName})' 237 | PrivateSubnet2: 238 | Condition: CondPrivateSubnet 239 | Type: 'AWS::EC2::Subnet' 240 | Properties: 241 | AvailabilityZone: !Select [1, !Ref AZs] 242 | CidrBlock: !Ref PrivateSubnet2Cidr 243 | MapPublicIpOnLaunch: false 244 | VpcId: !Ref VPC 245 | Tags: 246 | - Key: Application 247 | Value: !Ref 'AWS::StackId' 248 | - Key: Name 249 | Value: !Sub 'Private subnet 2 (${AWS::StackName})' 250 | PrivateSubnet3: 251 | Condition: CondPrivateSubnet&3AZ 252 | Type: 'AWS::EC2::Subnet' 253 | Properties: 254 | AvailabilityZone: !Select [2, !Ref AZs] 255 | CidrBlock: !Ref PrivateSubnet3Cidr 256 | MapPublicIpOnLaunch: false 257 | VpcId: !Ref VPC 258 | Tags: 259 | - Key: Application 260 | Value: !Ref 'AWS::StackId' 261 | - Key: Name 262 | Value: !Sub 'Private subnet 3 (${AWS::StackName})' 263 | PrivateSubnet4: 264 | Condition: CondPrivateSubnet&4AZ 265 | Type: 'AWS::EC2::Subnet' 266 | Properties: 267 | AvailabilityZone: !Select [3, !Ref AZs] 268 | CidrBlock: !Ref PrivateSubnet4Cidr 269 | MapPublicIpOnLaunch: false 270 | VpcId: !Ref VPC 271 | Tags: 272 | - Key: Application 273 | Value: !Ref 'AWS::StackId' 274 | - Key: Name 275 | Value: !Sub 'Private subnet 4 (${AWS::StackName})' 276 | PublicSubnet1: 277 | Type: 'AWS::EC2::Subnet' 278 | Properties: 279 | AvailabilityZone: !Select [0, !Ref AZs] 280 | CidrBlock: !Ref PublicSubnet1Cidr 281 | MapPublicIpOnLaunch: true 282 | VpcId: !Ref VPC 283 | Tags: 284 | - Key: Application 285 | Value: !Ref 'AWS::StackId' 286 | - Key: Name 287 | Value: !Sub 'Public subnet 1 (${AWS::StackName})' 288 | PublicSubnet2: 289 | Type: 'AWS::EC2::Subnet' 290 | Properties: 291 | AvailabilityZone: !Select [1, !Ref AZs] 292 | CidrBlock: !Ref PublicSubnet2Cidr 293 | MapPublicIpOnLaunch: true 294 | VpcId: !Ref VPC 295 | Tags: 296 | - Key: Application 297 | Value: !Ref 'AWS::StackId' 298 | - Key: Name 299 | Value: !Sub 'Public subnet 2 (${AWS::StackName})' 300 | PublicSubnet3: 301 | Condition: Cond3AZ 302 | Type: 'AWS::EC2::Subnet' 303 | Properties: 304 | AvailabilityZone: !Select [2, !Ref AZs] 305 | CidrBlock: !Ref PublicSubnet3Cidr 306 | MapPublicIpOnLaunch: true 307 | VpcId: !Ref VPC 308 | Tags: 309 | - Key: Application 310 | Value: !Ref 'AWS::StackId' 311 | - Key: Name 312 | Value: !Sub 'Public subnet 3 (${AWS::StackName})' 313 | PublicSubnet4: 314 | Condition: Cond4AZ 315 | Type: 'AWS::EC2::Subnet' 316 | Properties: 317 | AvailabilityZone: !Select [3, !Ref AZs] 318 | CidrBlock: !Ref PublicSubnet4Cidr 319 | MapPublicIpOnLaunch: true 320 | VpcId: !Ref VPC 321 | Tags: 322 | - Key: Application 323 | Value: !Ref 'AWS::StackId' 324 | - Key: Name 325 | Value: !Sub 'Public subnet 4 (${AWS::StackName})' 326 | PublicSubnetRouteTable: 327 | Type: 'AWS::EC2::RouteTable' 328 | Properties: 329 | VpcId: !Ref VPC 330 | Tags: 331 | - Key: Application 332 | Value: !Ref 'AWS::StackId' 333 | - Key: Name 334 | Value: !Sub 'Public RT (${AWS::StackName})' 335 | PublicSubnetRoute: 336 | Type: 'AWS::EC2::Route' 337 | DependsOn: AttachGateway 338 | Properties: 339 | RouteTableId: !Ref PublicSubnetRouteTable 340 | DestinationCidrBlock: 0.0.0.0/0 341 | GatewayId: !Ref InternetGateway 342 | PublicSubnet1RouteTableAssociation: 343 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 344 | Properties: 345 | SubnetId: !Ref PublicSubnet1 346 | RouteTableId: !Ref PublicSubnetRouteTable 347 | PublicSubnet2RouteTableAssociation: 348 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 349 | Properties: 350 | SubnetId: !Ref PublicSubnet2 351 | RouteTableId: !Ref PublicSubnetRouteTable 352 | PublicSubnet3RouteTableAssociation: 353 | Condition: Cond3AZ 354 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 355 | Properties: 356 | SubnetId: !Ref PublicSubnet3 357 | RouteTableId: !Ref PublicSubnetRouteTable 358 | PublicSubnet4RouteTableAssociation: 359 | Condition: Cond4AZ 360 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 361 | Properties: 362 | SubnetId: !Ref PublicSubnet4 363 | RouteTableId: !Ref PublicSubnetRouteTable 364 | NAT1EIP: 365 | Condition: CondPrivateSubnet 366 | DependsOn: AttachGateway 367 | Type: 'AWS::EC2::EIP' 368 | Properties: 369 | Domain: vpc 370 | NAT2EIP: 371 | Condition: CondPrivateSubnet 372 | DependsOn: AttachGateway 373 | Type: 'AWS::EC2::EIP' 374 | Properties: 375 | Domain: vpc 376 | NAT3EIP: 377 | Condition: CondPrivateSubnet&3AZ 378 | DependsOn: AttachGateway 379 | Type: 'AWS::EC2::EIP' 380 | Properties: 381 | Domain: vpc 382 | NAT4EIP: 383 | Condition: CondPrivateSubnet&4AZ 384 | DependsOn: AttachGateway 385 | Type: 'AWS::EC2::EIP' 386 | Properties: 387 | Domain: vpc 388 | NATGateway1: 389 | Condition: CondNATGateway 390 | DependsOn: AttachGateway 391 | Type: 'AWS::EC2::NatGateway' 392 | Properties: 393 | AllocationId: !GetAtt NAT1EIP.AllocationId 394 | SubnetId: !Ref PublicSubnet1 395 | NATGateway2: 396 | Condition: CondNATGateway 397 | DependsOn: AttachGateway 398 | Type: 'AWS::EC2::NatGateway' 399 | Properties: 400 | AllocationId: !GetAtt NAT2EIP.AllocationId 401 | SubnetId: !Ref PublicSubnet2 402 | NATGateway3: 403 | Condition: CondNATGateway&3AZ 404 | DependsOn: AttachGateway 405 | Type: 'AWS::EC2::NatGateway' 406 | Properties: 407 | AllocationId: !GetAtt NAT3EIP.AllocationId 408 | SubnetId: !Ref PublicSubnet3 409 | NATGateway4: 410 | Condition: CondNATGateway&4AZ 411 | DependsOn: AttachGateway 412 | Type: 'AWS::EC2::NatGateway' 413 | Properties: 414 | AllocationId: !GetAtt NAT4EIP.AllocationId 415 | SubnetId: !Ref PublicSubnet4 416 | PrivateSubnet1RouteTable: 417 | Condition: CondPrivateSubnet 418 | Type: 'AWS::EC2::RouteTable' 419 | Properties: 420 | VpcId: !Ref VPC 421 | Tags: 422 | - Key: Name 423 | Value: !Sub 'Private RT 1 (${AWS::StackName})' 424 | PrivateSubnet1Route: 425 | Condition: CondPrivateSubnet 426 | Type: 'AWS::EC2::Route' 427 | Properties: 428 | RouteTableId: !Ref PrivateSubnet1RouteTable 429 | DestinationCidrBlock: 0.0.0.0/0 430 | NatGatewayId: !Ref NATGateway1 431 | PrivateSubnet1RouteTableAssociation: 432 | Condition: CondPrivateSubnet 433 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 434 | Properties: 435 | SubnetId: !Ref PrivateSubnet1 436 | RouteTableId: !Ref PrivateSubnet1RouteTable 437 | PrivateSubnet2RouteTable: 438 | Condition: CondPrivateSubnet 439 | Type: 'AWS::EC2::RouteTable' 440 | Properties: 441 | VpcId: !Ref VPC 442 | Tags: 443 | - Key: Name 444 | Value: !Sub 'Private RT 2 (${AWS::StackName})' 445 | PrivateSubnet2Route: 446 | Condition: CondPrivateSubnet 447 | Type: 'AWS::EC2::Route' 448 | Properties: 449 | RouteTableId: !Ref PrivateSubnet2RouteTable 450 | DestinationCidrBlock: 0.0.0.0/0 451 | NatGatewayId: !Ref NATGateway2 452 | PrivateSubnet2RouteTableAssociation: 453 | Condition: CondPrivateSubnet 454 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 455 | Properties: 456 | SubnetId: !Ref PrivateSubnet2 457 | RouteTableId: !Ref PrivateSubnet2RouteTable 458 | PrivateSubnet3RouteTable: 459 | Condition: CondPrivateSubnet&3AZ 460 | Type: 'AWS::EC2::RouteTable' 461 | Properties: 462 | VpcId: !Ref VPC 463 | Tags: 464 | - Key: Name 465 | Value: !Sub 'Private RT 3 (${AWS::StackName})' 466 | PrivateSubnet3Route: 467 | Condition: CondPrivateSubnet&3AZ 468 | Type: 'AWS::EC2::Route' 469 | Properties: 470 | RouteTableId: !Ref PrivateSubnet3RouteTable 471 | DestinationCidrBlock: 0.0.0.0/0 472 | NatGatewayId: !Ref NATGateway3 473 | PrivateSubnet3RouteTableAssociation: 474 | Condition: CondPrivateSubnet&3AZ 475 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 476 | Properties: 477 | SubnetId: !Ref PrivateSubnet3 478 | RouteTableId: !Ref PrivateSubnet3RouteTable 479 | PrivateSubnet4RouteTable: 480 | Condition: CondPrivateSubnet&4AZ 481 | Type: 'AWS::EC2::RouteTable' 482 | Properties: 483 | VpcId: !Ref VPC 484 | Tags: 485 | - Key: Name 486 | Value: !Sub 'Private RT 4 (${AWS::StackName})' 487 | PrivateSubnet4Route: 488 | Condition: CondPrivateSubnet&4AZ 489 | Type: 'AWS::EC2::Route' 490 | Properties: 491 | RouteTableId: !Ref PrivateSubnet4RouteTable 492 | DestinationCidrBlock: 0.0.0.0/0 493 | NatGatewayId: !Ref NATGateway4 494 | PrivateSubnet4RouteTableAssociation: 495 | Condition: CondPrivateSubnet&4AZ 496 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 497 | Properties: 498 | SubnetId: !Ref PrivateSubnet4 499 | RouteTableId: !Ref PrivateSubnet4RouteTable 500 | Outputs: 501 | VPC: 502 | Description: Newly created VPC 503 | Value: !Ref VPC 504 | Export: 505 | Name: !Sub '${AWS::StackName}-VPC' 506 | PublicSubnet1: 507 | Description: Public Subnet 1 508 | Value: !Ref PublicSubnet1 509 | Export: 510 | Name: !Sub '${AWS::StackName}-PublicSubnet1' 511 | PublicSubnet2: 512 | Description: Public Subnet 2 513 | Value: !Ref PublicSubnet2 514 | Export: 515 | Name: !Sub '${AWS::StackName}-PublicSubnet2' 516 | PublicSubnet3: 517 | Condition: Cond3AZ 518 | Description: Public Subnet 3 519 | Value: !Ref PublicSubnet3 520 | Export: 521 | Name: !Sub '${AWS::StackName}-PublicSubnet3' 522 | PublicSubnet4: 523 | Condition: Cond4AZ 524 | Description: Public Subnet 4 525 | Value: !Ref PublicSubnet4 526 | Export: 527 | Name: !Sub '${AWS::StackName}-PublicSubnet4' 528 | PrivateSubnet1: 529 | Condition: CondPrivateSubnet 530 | Description: Private Subnet 1 531 | Value: !Ref PrivateSubnet1 532 | Export: 533 | Name: !Sub '${AWS::StackName}-PrivateSubnet1' 534 | PrivateSubnet2: 535 | Condition: CondPrivateSubnet 536 | Description: Private Subnet 2 537 | Value: !Ref PrivateSubnet2 538 | Export: 539 | Name: !Sub '${AWS::StackName}-PrivateSubnet2' 540 | PrivateSubnet3: 541 | Condition: CondPrivateSubnet&3AZ 542 | Description: Private Subnet 3 543 | Value: !Ref PrivateSubnet3 544 | Export: 545 | Name: !Sub '${AWS::StackName}-PrivateSubnet3' 546 | PrivateSubnet4: 547 | Condition: CondPrivateSubnet&4AZ 548 | Description: Private Subnet 4 549 | Value: !Ref PrivateSubnet4 550 | Export: 551 | Name: !Sub '${AWS::StackName}-PrivateSubnet4' 552 | NAT1EIP: 553 | Condition: CondPrivateSubnet 554 | Description: NAT 1 IP address 555 | Value: !Ref NAT1EIP 556 | Export: 557 | Name: !Sub '${AWS::StackName}-NAT1EIP' 558 | NAT2EIP: 559 | Condition: CondPrivateSubnet 560 | Description: NAT 2 IP address 561 | Value: !Ref NAT2EIP 562 | Export: 563 | Name: !Sub '${AWS::StackName}-NAT2EIP' 564 | NAT3EIP: 565 | Condition: CondPrivateSubnet&3AZ 566 | Description: NAT 3 IP address 567 | Value: !Ref NAT3EIP 568 | Export: 569 | Name: !Sub '${AWS::StackName}-NAT3EIP' 570 | NAT4EIP: 571 | Condition: CondPrivateSubnet&4AZ 572 | Description: NAT 4 IP address 573 | Value: !Ref NAT4EIP 574 | Export: 575 | Name: !Sub '${AWS::StackName}-NAT4EIP' 576 | -------------------------------------------------------------------------------- /src/azure/k8s-deployment.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | 8 | # WhatsApp Business API Azure Template Version 1.0.1 9 | 10 | locals { 11 | root-vol = "root-vol" 12 | root-vol-src = "/" 13 | root-vol-container-path = "/rootfs" 14 | tmpfs-vol = "tmpfs" 15 | tmpfs-vol-src = "/var/run" 16 | tmpfs-vol-container-path = "/var/run" 17 | sys-vol = "sys-vol" 18 | sys-vol-src = "/sys" 19 | sys-vol-container-path = "/host/sys" 20 | docker-vol = "docker-vol" 21 | docker-vol-src = "/var/lib/docker" 22 | docker-vol-container-path = "/var/lib/docker" 23 | device-vol = "device-vol" 24 | device-vol-src = "/dev/disk" 25 | device-vol-container-path = "/dev/disk" 26 | proc-vol = "proc-vol" 27 | proc-vol-src = "/proc" 28 | proc-vol-container-path = "/host/proc" 29 | } 30 | 31 | locals { 32 | prom-vol = "prom-vol" 33 | prom-vol-src = "/prometheus-data" 34 | prom-vol-container-path = "/prometheus-data" 35 | grafana-vol = "grafana-vol" 36 | grafana-vol-src = "/var/lib/grafana" 37 | grafana-vol-container-path = "/var/lib/grafana" 38 | } 39 | 40 | 41 | locals { 42 | number_of_masterapp = 2 43 | # mysql_credential_vol = "mysql-credential-vol" 44 | mysql_credential_mount_path = "/var/mysql/credential" 45 | mysql_init_vol = "mysql-init-vol" 46 | mysql_init_mount_path = "/var/mysql/init" 47 | db_vol = "db-vol" 48 | db_mount_path = "/var/lib/mysql" 49 | db_sub_path = "mysql" 50 | db_config_mount_path = "/etc/mysql" 51 | db_config_sub_path = "etc/mysql" 52 | media_mount_path = "/usr/local/wamedia" 53 | media_vol = "media-vol" 54 | media_sub_path = "waent/media" 55 | data_mount_path = "/usr/local/waent/data" 56 | data_sub_path = "waent/data" 57 | config_map_ref_name = "config-env" 58 | config_map_ref_name_master = "config-master" 59 | secret_map_ref_name = "secret-env" 60 | init_cmd = "export WA_DB_SSL_CA= && cd /opt/whatsapp/bin && ./launch_within_docker.sh" #DB in VM 61 | init_cmd_coreapp = "export WA_DB_SSL_CA= && cd /opt/whatsapp/bin && IP=$(hostname -I | awk '{print $1}') && export COREAPP_HOSTNAME=$IP && ./launch_within_docker.sh" #DB in VM 62 | # init_cmd = "mkdir -p /opt/certs && echo \"Downloading CA bundle ...\" >> /var/log/whatsapp.log && cd /opt/certs && wget ${var.DBCertURL} -O ${var.DBConnCA} && ls -al ${var.DBConnCA} >> /var/log/whatsapp.log && cd /opt/whatsapp/bin && ./launch_within_docker.sh" # && while true; do sleep 30; done;" # && ./launch_within_docker.sh" 63 | # init_cmd_coreapp = "mkdir -p /opt/certs && echo \"Downloading CA bundle ...\" >> /var/log/whatsapp.log && cd /opt/certs && wget ${var.DBCertURL} -O ${var.DBConnCA} && ls -al ${var.DBConnCA} >> /var/log/whatsapp.log && cd /opt/whatsapp/bin && IP=$(hostname -I) && export COREAPP_HOSTNAME=$IP && ./launch_within_docker.sh" 64 | } 65 | 66 | resource "kubernetes_deployment" "webapp" { 67 | timeouts { 68 | create = "2m" 69 | update = "2m" 70 | delete = "1m" 71 | } 72 | metadata { 73 | name = "webapp" 74 | # namespace = kubernetes_namespace.deployment.metadata.0.name 75 | labels = { 76 | type = "webapp" 77 | } 78 | } 79 | spec { 80 | replicas = var.map_web_server_count[var.throughput] 81 | selector { 82 | match_labels = { 83 | type = "webapp" 84 | } 85 | } 86 | 87 | template { 88 | metadata { 89 | name = "webapp" 90 | labels = { type = "webapp" } 91 | } 92 | spec { 93 | affinity { 94 | pod_anti_affinity { 95 | required_during_scheduling_ignored_during_execution { 96 | label_selector { 97 | match_expressions { 98 | key = "type" 99 | operator = "In" 100 | values = ["webapp"] 101 | } 102 | } 103 | topology_key = "kubernetes.io/hostname" 104 | } 105 | } 106 | 107 | node_affinity { 108 | required_during_scheduling_ignored_during_execution { 109 | node_selector_term { 110 | match_expressions { 111 | key = "type" 112 | operator = "In" 113 | values = ["webapp"] 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | container { 121 | image = "docker.whatsapp.biz/web:${var.api-version}" 122 | name = "webapp" 123 | 124 | command = ["/bin/sh", "-c"] 125 | 126 | args = [ 127 | local.init_cmd 128 | ] 129 | 130 | volume_mount { 131 | name = local.media_vol 132 | mount_path = local.media_mount_path 133 | sub_path = local.media_sub_path 134 | } 135 | 136 | volume_mount { 137 | name = local.media_vol 138 | mount_path = local.data_mount_path 139 | sub_path = local.data_sub_path 140 | } 141 | 142 | env_from { 143 | config_map_ref { 144 | name = kubernetes_config_map.env.metadata.0.name 145 | } 146 | } 147 | 148 | env_from { 149 | secret_ref { 150 | name = kubernetes_secret.env.metadata.0.name 151 | } 152 | } 153 | 154 | port { 155 | container_port = 443 156 | } 157 | } 158 | 159 | volume { 160 | name = local.media_vol 161 | persistent_volume_claim { 162 | claim_name = kubernetes_persistent_volume_claim.media-share.metadata.0.name 163 | } 164 | } 165 | } 166 | } 167 | 168 | strategy { 169 | type = "RollingUpdate" 170 | 171 | rolling_update { 172 | max_unavailable = "1" 173 | max_surge = "1" 174 | } 175 | } 176 | revision_history_limit = 10 177 | } 178 | depends_on = [azurerm_kubernetes_cluster_node_pool.webapp] 179 | } 180 | 181 | resource "kubernetes_deployment" "coreapp" { 182 | timeouts { 183 | create = "10m" 184 | update = "3m" 185 | delete = "1m" 186 | } 187 | metadata { 188 | name = "coreapp" 189 | # namespace = kubernetes_namespace.deployment.metadata.0.name 190 | labels = { 191 | type = "coreapp" 192 | } 193 | } 194 | 195 | spec { 196 | replicas = var.map_shards_count[var.throughput] + 1 // one more for disconnected HA coreapp 197 | selector { 198 | match_labels = { 199 | type = "coreapp" 200 | } 201 | } 202 | 203 | template { 204 | metadata { 205 | name = "coreapp" 206 | labels = { type = "coreapp" } 207 | } 208 | 209 | spec { 210 | affinity { 211 | pod_anti_affinity { 212 | required_during_scheduling_ignored_during_execution { 213 | label_selector { 214 | match_expressions { 215 | key = "type" 216 | operator = "In" 217 | values = ["coreapp"] 218 | } 219 | } 220 | topology_key = "kubernetes.io/hostname" 221 | } 222 | } 223 | node_affinity { 224 | required_during_scheduling_ignored_during_execution { 225 | node_selector_term { 226 | match_expressions { 227 | key = "type" 228 | operator = "In" 229 | values = ["coreapp"] 230 | } 231 | } 232 | } 233 | } 234 | } 235 | 236 | volume { 237 | name = local.media_vol 238 | persistent_volume_claim { 239 | claim_name = kubernetes_persistent_volume_claim.media-share.metadata.0.name 240 | } 241 | } 242 | 243 | container { 244 | image = "docker.whatsapp.biz/coreapp:${var.api-version}" 245 | name = "coreapp" 246 | 247 | command = ["/bin/sh", "-c"] 248 | 249 | args = [ 250 | local.init_cmd_coreapp 251 | ] 252 | 253 | volume_mount { 254 | name = local.media_vol 255 | mount_path = local.media_mount_path 256 | sub_path = local.media_sub_path 257 | } 258 | 259 | volume_mount { 260 | name = local.media_vol 261 | mount_path = local.data_mount_path 262 | sub_path = local.data_sub_path 263 | } 264 | 265 | env_from { 266 | config_map_ref { 267 | name = local.config_map_ref_name 268 | } 269 | } 270 | 271 | env_from { 272 | secret_ref { 273 | name = local.secret_map_ref_name 274 | } 275 | } 276 | 277 | port { 278 | container_port = 6250 279 | } 280 | port { 281 | container_port = 6251 282 | } 283 | port { 284 | container_port = 6252 285 | } 286 | port { 287 | container_port = 6253 288 | } 289 | } 290 | } 291 | } 292 | 293 | strategy { 294 | type = "RollingUpdate" 295 | 296 | rolling_update { 297 | max_unavailable = "1" 298 | max_surge = "1" 299 | } 300 | } 301 | revision_history_limit = 10 302 | } 303 | 304 | depends_on = [azurerm_kubernetes_cluster_node_pool.coreapp] 305 | 306 | } 307 | 308 | resource "kubernetes_deployment" "masterapp" { 309 | timeouts { 310 | create = "1m" 311 | update = "2m" 312 | delete = "1m" 313 | } 314 | metadata { 315 | name = "masterapp" 316 | # namespace = kubernetes_namespace.deployment.metadata.0.name 317 | labels = { 318 | type = "masterapp" 319 | } 320 | } 321 | 322 | spec { 323 | replicas = local.number_of_masterapp 324 | selector { 325 | match_labels = { 326 | type = "masterapp" 327 | } 328 | } 329 | 330 | template { 331 | metadata { 332 | name = "masterapp" 333 | labels = { type = "masterapp" } 334 | } 335 | 336 | spec { 337 | affinity { 338 | pod_anti_affinity { 339 | required_during_scheduling_ignored_during_execution { 340 | label_selector { 341 | match_expressions { 342 | key = "type" 343 | operator = "In" 344 | values = ["masterapp"] 345 | } 346 | } 347 | topology_key = "kubernetes.io/hostname" 348 | } 349 | } 350 | node_affinity { 351 | required_during_scheduling_ignored_during_execution { 352 | node_selector_term { 353 | match_expressions { 354 | key = "type" 355 | operator = "In" 356 | values = ["webapp"] 357 | } 358 | } 359 | } 360 | } 361 | } 362 | 363 | container { 364 | image = "docker.whatsapp.biz/coreapp:${var.api-version}" 365 | name = "masterapp" 366 | 367 | command = ["/bin/sh", "-c"] 368 | 369 | args = [ 370 | local.init_cmd_coreapp 371 | ] 372 | 373 | env_from { 374 | config_map_ref { 375 | name = local.config_map_ref_name 376 | } 377 | } 378 | 379 | env_from { 380 | config_map_ref { 381 | name = local.config_map_ref_name_master 382 | } 383 | } 384 | 385 | env_from { 386 | secret_ref { 387 | name = local.secret_map_ref_name 388 | } 389 | } 390 | 391 | port { 392 | container_port = 6250 393 | } 394 | port { 395 | container_port = 6251 396 | } 397 | port { 398 | container_port = 6252 399 | } 400 | port { 401 | container_port = 6253 402 | } 403 | } 404 | 405 | } 406 | } 407 | 408 | strategy { 409 | type = "RollingUpdate" 410 | 411 | rolling_update { 412 | max_unavailable = "1" 413 | max_surge = "1" 414 | } 415 | } 416 | revision_history_limit = 10 417 | } 418 | depends_on = [azurerm_kubernetes_cluster_node_pool.coreapp] 419 | } 420 | 421 | resource "kubernetes_deployment" "monitor" { 422 | timeouts { 423 | create = "2m" 424 | update = "1m" 425 | delete = "1m" 426 | } 427 | metadata { 428 | name = "monitor" 429 | # namespace = kubernetes_namespace.deployment.metadata.0.name 430 | labels = { 431 | type = "monitor" 432 | } 433 | } 434 | spec { 435 | replicas = 1 436 | selector { 437 | match_labels = { 438 | type = "monitor" 439 | } 440 | } 441 | 442 | template { 443 | metadata { 444 | name = "monitor" 445 | labels = { type = "monitor" } 446 | } 447 | 448 | spec { 449 | affinity { 450 | node_affinity { 451 | required_during_scheduling_ignored_during_execution { 452 | node_selector_term { 453 | match_expressions { 454 | key = "type" 455 | operator = "In" 456 | values = ["monitor"] 457 | } 458 | } 459 | } 460 | } 461 | } 462 | 463 | volume { 464 | name = local.prom-vol 465 | host_path { 466 | path = local.prom-vol-src 467 | } 468 | } 469 | 470 | volume { 471 | name = local.grafana-vol 472 | host_path { 473 | path = local.grafana-vol-src 474 | } 475 | } 476 | 477 | container { 478 | image = "prom/mysqld-exporter:v0.10.0" 479 | name = "mysqld-exporter" 480 | 481 | resources { 482 | requests = { 483 | memory = "1Gi" 484 | } 485 | } 486 | 487 | env_from { 488 | secret_ref { 489 | name = kubernetes_secret.db.metadata.0.name 490 | } 491 | } 492 | 493 | port { 494 | container_port = 9104 495 | } 496 | } 497 | 498 | container { 499 | image = "docker.whatsapp.biz/prometheus:${var.api-version}" 500 | name = "prometheus" 501 | 502 | # command = ["/bin/sh", "-c", "while true; do sleep 30; done;"] 503 | 504 | security_context { 505 | run_as_user = 0 506 | } 507 | 508 | volume_mount { 509 | name = local.prom-vol 510 | mount_path = local.prom-vol-container-path 511 | } 512 | 513 | resources { 514 | requests = { 515 | memory = "1Gi" 516 | } 517 | } 518 | 519 | env_from { 520 | config_map_ref { 521 | name = kubernetes_config_map.mon-prom.metadata.0.name 522 | } 523 | } 524 | 525 | env_from { 526 | secret_ref { 527 | name = kubernetes_secret.mon-prom.metadata.0.name 528 | } 529 | } 530 | 531 | port { 532 | container_port = 9090 533 | } 534 | } 535 | 536 | 537 | container { 538 | image = "docker.whatsapp.biz/grafana:${var.api-version}" 539 | name = "grafana" 540 | 541 | volume_mount { 542 | name = local.grafana-vol 543 | mount_path = local.grafana-vol-container-path 544 | } 545 | 546 | resources { 547 | requests = { 548 | memory = "1Gi" 549 | } 550 | } 551 | 552 | env_from { 553 | config_map_ref { 554 | name = kubernetes_config_map.mon-graf.metadata.0.name 555 | } 556 | } 557 | 558 | env_from { 559 | secret_ref { 560 | name = kubernetes_secret.mon-graf.metadata.0.name 561 | } 562 | } 563 | 564 | port { 565 | container_port = 3000 566 | } 567 | } 568 | 569 | 570 | 571 | } 572 | } 573 | 574 | strategy { 575 | type = "RollingUpdate" 576 | 577 | rolling_update { 578 | max_unavailable = "1" 579 | max_surge = "1" 580 | } 581 | } 582 | revision_history_limit = 10 583 | } 584 | 585 | depends_on = [ 586 | kubernetes_deployment.coreapp, 587 | kubernetes_deployment.masterapp, 588 | kubernetes_deployment.webapp 589 | ] 590 | } 591 | 592 | resource "kubernetes_daemonset" "exporter" { 593 | metadata { 594 | name = "exporter" 595 | labels = { 596 | type = "exporter" 597 | } 598 | } 599 | 600 | spec { 601 | selector { 602 | match_labels = { 603 | type = "exporter" 604 | } 605 | } 606 | 607 | template { 608 | metadata { 609 | labels = { 610 | type = "exporter" 611 | } 612 | } 613 | 614 | spec { 615 | affinity { 616 | node_affinity { 617 | required_during_scheduling_ignored_during_execution { 618 | node_selector_term { 619 | match_expressions { 620 | key = "type" 621 | operator = "In" 622 | values = ["coreapp", "webapp"] 623 | } 624 | } 625 | } 626 | } 627 | } 628 | 629 | volume { 630 | name = local.root-vol 631 | host_path { 632 | path = local.root-vol-src 633 | } 634 | } 635 | 636 | volume { 637 | name = local.tmpfs-vol 638 | host_path { 639 | path = local.tmpfs-vol-src 640 | } 641 | } 642 | 643 | volume { 644 | name = local.sys-vol 645 | host_path { 646 | path = local.sys-vol-src 647 | } 648 | } 649 | 650 | volume { 651 | name = local.docker-vol 652 | host_path { 653 | path = local.docker-vol-src 654 | } 655 | } 656 | 657 | volume { 658 | name = local.device-vol 659 | host_path { 660 | path = local.device-vol-src 661 | } 662 | } 663 | 664 | volume { 665 | name = local.proc-vol 666 | host_path { 667 | path = local.proc-vol-src 668 | } 669 | } 670 | 671 | container { 672 | image = "google/cadvisor:v0.30.2" 673 | name = "cadvisor" 674 | 675 | volume_mount { 676 | name = local.root-vol 677 | mount_path = local.root-vol-container-path 678 | } 679 | 680 | volume_mount { 681 | name = local.sys-vol 682 | mount_path = local.sys-vol-container-path 683 | } 684 | 685 | volume_mount { 686 | name = local.docker-vol 687 | mount_path = local.docker-vol-container-path 688 | } 689 | 690 | volume_mount { 691 | name = local.device-vol 692 | mount_path = local.device-vol-container-path 693 | } 694 | 695 | volume_mount { 696 | name = local.tmpfs-vol 697 | mount_path = local.tmpfs-vol-container-path 698 | } 699 | 700 | resources { 701 | requests = { 702 | memory = "128Mi" 703 | } 704 | } 705 | 706 | port { 707 | container_port = 8080 708 | } 709 | } 710 | 711 | container { 712 | image = "prom/node-exporter:v0.16.0" 713 | name = "node-exporter" 714 | 715 | volume_mount { 716 | name = local.root-vol 717 | mount_path = local.root-vol-container-path 718 | read_only = true 719 | } 720 | 721 | volume_mount { 722 | name = local.sys-vol 723 | mount_path = local.sys-vol-container-path 724 | read_only = true 725 | } 726 | 727 | volume_mount { 728 | name = local.proc-vol 729 | mount_path = local.proc-vol-container-path 730 | read_only = true 731 | } 732 | 733 | resources { 734 | requests = { 735 | memory = "32Mi" 736 | } 737 | } 738 | 739 | port { 740 | container_port = 9100 741 | } 742 | } 743 | 744 | } 745 | } 746 | } 747 | } 748 | 749 | resource "kubernetes_stateful_set" "db" { 750 | timeouts { 751 | create = "5m" 752 | update = "1m" 753 | } 754 | metadata { 755 | name = "db" 756 | labels = { 757 | type = "db" 758 | } 759 | } 760 | 761 | spec { 762 | pod_management_policy = "Parallel" 763 | replicas = 1 764 | revision_history_limit = 5 765 | 766 | selector { 767 | match_labels = { 768 | type = "db" 769 | } 770 | } 771 | 772 | service_name = kubernetes_service.db.metadata.0.name 773 | template { 774 | metadata { 775 | labels = { 776 | type = "db" 777 | } 778 | 779 | annotations = {} 780 | } 781 | 782 | spec { 783 | container { 784 | name = "db-server" 785 | image = "mysql:8.0.15" 786 | image_pull_policy = "IfNotPresent" 787 | 788 | # command = ["/bin/sh", "-c", "while true; do sleep 300; done;"] 789 | 790 | lifecycle { 791 | post_start { 792 | exec { 793 | command = ["/bin/bash", "-c", "/var/mysql/init/copy-cnf.sh"] 794 | } 795 | } 796 | } 797 | 798 | port { 799 | container_port = 3306 800 | } 801 | 802 | env { 803 | name = "MYSQL_ROOT_PASSWORD" 804 | value = var.dbpassword 805 | } 806 | 807 | env_from { 808 | secret_ref { 809 | name = local.secret_map_ref_name 810 | } 811 | } 812 | 813 | volume_mount { 814 | name = local.db_vol 815 | mount_path = local.db_mount_path 816 | sub_path = local.db_sub_path 817 | } 818 | 819 | volume_mount { 820 | name = local.db_vol 821 | mount_path = local.db_config_mount_path 822 | sub_path = local.db_config_sub_path 823 | } 824 | 825 | volume_mount { 826 | name = local.mysql_init_vol 827 | mount_path = local.mysql_init_mount_path 828 | } 829 | 830 | volume_mount { 831 | name = "mysql-initdb" 832 | mount_path = "/docker-entrypoint-initdb.d" 833 | } 834 | 835 | # volume_mount { 836 | # name = local.mysql_credential_vol 837 | # mount_path = local.mysql_credential_mount_path 838 | # } 839 | } 840 | 841 | termination_grace_period_seconds = 300 842 | 843 | volume { 844 | name = local.db_vol 845 | persistent_volume_claim { 846 | claim_name = kubernetes_persistent_volume_claim.db.metadata.0.name 847 | } 848 | } 849 | 850 | volume { 851 | name = local.mysql_init_vol 852 | config_map { 853 | name = kubernetes_config_map.mysql-init.metadata.0.name 854 | default_mode = "0777" 855 | } 856 | } 857 | 858 | volume { 859 | name = "mysql-initdb" 860 | config_map { 861 | name = kubernetes_config_map.mysql-initdb.metadata.0.name 862 | default_mode = "0777" 863 | } 864 | } 865 | 866 | # volume { 867 | # name = local.mysql_credential_vol 868 | # secret { 869 | # secret_name = kubernetes_secret.env.metadata.0.name 870 | # } 871 | # } 872 | } 873 | } 874 | 875 | update_strategy { 876 | type = "RollingUpdate" 877 | 878 | rolling_update { 879 | partition = 1 880 | } 881 | } 882 | } 883 | } 884 | -------------------------------------------------------------------------------- /src/aws/wa_ent_monitoring.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | # 6 | 7 | # WhatsApp Business API Instance Monitoring CFN Version 1.0.2 8 | AWSTemplateFormatVersion: "2010-09-09" 9 | Description: >- 10 | AWS CloudFormation template to create instance monitoring for 11 | WhatsApp Business API Client with Prometheus and Grafana. 12 | This template assumes WhatsApp Business API Client has been created and is running. 13 | 14 | Metadata: 15 | AWS::CloudFormation::Interface: 16 | ParameterGroups: 17 | - Label: 18 | default: "Network configuration" 19 | Parameters: 20 | - VpcId 21 | - DeploymentSubnet 22 | - LBScheme 23 | - LBSubnet 24 | - Label: 25 | default: "Monitoring stack configuration" 26 | Parameters: 27 | - KeyName 28 | - WAEntContRegistry 29 | - WAEntContTag 30 | - Label: 31 | default: "WhatsApp Business API database info" 32 | Parameters: 33 | - DBUser 34 | - DBPassword 35 | - DBHostname 36 | - DBPort 37 | - Label: 38 | default: "WhatsApp Business API access point" 39 | Parameters: 40 | - WAWebUsername 41 | - WAWebPassword 42 | - WAEntLB 43 | - Label: 44 | default: "Grafana" 45 | Parameters: 46 | - GrafanaAdminPassword 47 | - GrafanaEnableSmtp 48 | - GrafanaSmtpHost 49 | - GrafanaSmtpUser 50 | - GrafanaSmtpPassword 51 | 52 | Parameters: 53 | KeyName: 54 | Type: AWS::EC2::KeyPair::KeyName 55 | Description: Name of an existing EC2 KeyPair to enable SSH access to the monitoring EC2 instance 56 | VpcId: 57 | Type: AWS::EC2::VPC::Id 58 | Description: Select a VPC that has access to WAEntLB and DBHostname of the WhatsApp Business API stack 59 | DeploymentSubnet: 60 | Type: AWS::EC2::Subnet::Id 61 | Description: Select 1 subnet in the selected VPC to deploy the monitoring EC2 instance. 62 | LBScheme: 63 | Description: LoadBalancer scheme for the Grafana dashboard. Internal (private) or internet-facing (public) 64 | Type: String 65 | Default: internet-facing 66 | AllowedValues: 67 | - internal 68 | - internet-facing 69 | ConstraintDescription: Please choose a valid LoadBalancer scheme 70 | LBSubnet: 71 | Type: AWS::EC2::Subnet::Id 72 | Description: Select 1 subnet for the load balancer. Must match LBScheme - public for internet-facing, and private for internal. Must be in the same availability zone as DeploymentSubnet. 73 | 74 | WAEntContRegistry: 75 | Description: WhatsApp Business API docker registry 76 | Type: String 77 | Default: docker.whatsapp.biz 78 | 79 | WAEntContTag: 80 | Description: "Same as the version used for the WhatsApp Business API stack (e.g.: v2.37.1, do NOT use 'latest')" 81 | Type: String 82 | 83 | # DB 84 | DBHostname: 85 | Description: WA Biz API Existing DB hostname. 86 | Type: String 87 | DBPort: 88 | Description: WA Biz API Database port 89 | Type: Number 90 | Default: 3306 91 | MinValue: 1025 92 | MaxValue: 65535 93 | ConstraintDescription: must be a valid number between 1025-65535 94 | DBUser: 95 | NoEcho: "true" 96 | Description: Username to access WhatsApp Business API DB 97 | Type: String 98 | MinLength: "1" 99 | MaxLength: "16" 100 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 101 | ConstraintDescription: must begin with a letter and contain only alphanumeric characters. 102 | DBPassword: 103 | NoEcho: "true" 104 | Description: Password of DBUser 105 | Type: String 106 | MinLength: "8" 107 | MaxLength: "41" 108 | ConstraintDescription: must have at least 8 & at most 41 characters 109 | 110 | # Monitoring 111 | WAEntLB: 112 | Description: "WhatsApp Business API Load Balancer Endpoint" 113 | Type: String 114 | WAWebUsername: 115 | Description: "WhatsApp Business API Username" 116 | Type: String 117 | WAWebPassword: 118 | NoEcho: "true" 119 | Description: "Password for WAWebUsername" 120 | Type: String 121 | GrafanaAdminPassword: 122 | NoEcho: "true" 123 | Description: "You will use this as the login password for the Grafana dashboard when the stack is created" 124 | Type: String 125 | GrafanaEnableSmtp: 126 | Default: 0 127 | Description: "Enable SMTP for setting up email alert. Value 0 means disable, and 1 means enable" 128 | Type: Number 129 | AllowedValues: [0, 1] 130 | GrafanaSmtpHost: 131 | Description: "SMTP host used in email alert, e,g, smtp.gmail.com:465" 132 | Type: String 133 | GrafanaSmtpUser: 134 | Description: "SMTP user name used in email alert" 135 | Type: String 136 | GrafanaSmtpPassword: 137 | NoEcho: "true" 138 | Description: "SMTP password used in email alert" 139 | Type: String 140 | 141 | Mappings: 142 | AWSRegionToAMI: 143 | ap-south-1: 144 | AMIID: ami-064ac61091898694e 145 | eu-north-1: 146 | AMIID: ami-0e82a733fb827a38f 147 | eu-west-3: 148 | AMIID: ami-0e5736be34608455b 149 | eu-west-2: 150 | AMIID: ami-0aa938b5c246ef111 151 | eu-west-1: 152 | AMIID: ami-009f51225716cb42f 153 | ap-northeast-3: 154 | AMIID: ami-0bc54ca5eea7bb7b6 155 | ap-northeast-2: 156 | AMIID: ami-05ca84b0f62b22685 157 | ap-northeast-1: 158 | AMIID: ami-074c801439a538a43 159 | ca-central-1: 160 | AMIID: ami-0e9d8898441d3540d 161 | sa-east-1: 162 | AMIID: ami-02108349336c623e5 163 | ap-southeast-1: 164 | AMIID: ami-07dc7fbc73bffbeb5 165 | ap-southeast-2: 166 | AMIID: ami-0c0883406ea68dedc 167 | eu-central-1: 168 | AMIID: ami-049842bfd58f656fb 169 | us-east-1: 170 | AMIID: ami-0df2a11dd1fe1f8e3 171 | us-east-2: 172 | AMIID: ami-011d59a275b482a49 173 | us-west-1: 174 | AMIID: ami-084c852d3275c2e20 175 | us-west-2: 176 | AMIID: ami-094cc0ced7b91fcf0 177 | 178 | Resources: 179 | ECSCluster: 180 | Type: AWS::ECS::Cluster 181 | 182 | EcsSecurityGroup: 183 | Type: AWS::EC2::SecurityGroup 184 | Properties: 185 | GroupDescription: ECS Security Group 186 | VpcId: !Ref "VpcId" 187 | 188 | EcsSecurityGroupSSHinbound: 189 | Type: AWS::EC2::SecurityGroupIngress 190 | Properties: 191 | GroupId: !Ref "EcsSecurityGroup" 192 | IpProtocol: tcp 193 | FromPort: "22" 194 | ToPort: "22" 195 | CidrIp: 0.0.0.0/0 196 | 197 | EcsSecurityGroupGrafanaPorts: 198 | Type: AWS::EC2::SecurityGroupIngress 199 | Properties: 200 | GroupId: !Ref "EcsSecurityGroup" 201 | IpProtocol: tcp 202 | FromPort: "3000" 203 | ToPort: "3000" 204 | CidrIp: 0.0.0.0/0 205 | 206 | EcsSecurityGroupPrometheusPorts: 207 | Type: AWS::EC2::SecurityGroupIngress 208 | Properties: 209 | GroupId: !Ref "EcsSecurityGroup" 210 | IpProtocol: tcp 211 | FromPort: "9090" 212 | ToPort: "9090" 213 | CidrIp: 0.0.0.0/0 214 | 215 | CloudwatchLogsGroup: 216 | Type: AWS::Logs::LogGroup 217 | Properties: 218 | RetentionInDays: 14 219 | 220 | WAMonitoringSecret: 221 | Type: "AWS::SecretsManager::Secret" 222 | Properties: 223 | Name: !Sub "${AWS::StackName}-Secret" 224 | Description: Secret to store secrets required to create the monitoring stack, including DBPassword, WAWebPassword, GrafanaAdminPassword 225 | SecretString: !Join 226 | - "" 227 | - - '{"wa_web_pwd": "' 228 | - !Ref WAWebPassword 229 | - '", "grafana_pwd": "' 230 | - !Ref GrafanaAdminPassword 231 | - '", "grafana_smtp_pwd": "' 232 | - !Ref GrafanaSmtpPassword 233 | - '", "data_src_name": "' 234 | - !Join 235 | - "" 236 | - - !Ref DBUser 237 | - ":" 238 | - !Ref DBPassword 239 | - "@(" 240 | - !Ref DBHostname 241 | - ":" 242 | - !Ref DBPort 243 | - ")/" 244 | - '"}' 245 | 246 | WAMonitoringTaskDefinition: 247 | Type: AWS::ECS::TaskDefinition 248 | Properties: 249 | ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn 250 | ContainerDefinitions: 251 | - Name: mysqld-exporter 252 | Image: "prom/mysqld-exporter:v0.14.0" 253 | MemoryReservation: "1024" 254 | LogConfiguration: 255 | LogDriver: awslogs 256 | Options: 257 | awslogs-group: !Ref "CloudwatchLogsGroup" 258 | awslogs-region: !Ref "AWS::Region" 259 | awslogs-stream-prefix: mysql-exporter 260 | PortMappings: 261 | - ContainerPort: 9104 262 | HostPort: 9104 263 | Secrets: 264 | - Name: DATA_SOURCE_NAME 265 | ValueFrom: !Sub "${WAMonitoringSecret}:data_src_name::" 266 | - Name: prometheus 267 | Image: !Sub "${WAEntContRegistry}/prometheus:${WAEntContTag}" 268 | MemoryReservation: "1024" 269 | LogConfiguration: 270 | LogDriver: awslogs 271 | Options: 272 | awslogs-group: !Ref "CloudwatchLogsGroup" 273 | awslogs-region: !Ref "AWS::Region" 274 | awslogs-stream-prefix: prometheus 275 | User: "root" 276 | Links: ["mysqld-exporter"] 277 | MountPoints: 278 | - ContainerPath: /prometheus-data 279 | SourceVolume: prom-vol 280 | PortMappings: 281 | - ContainerPort: 9090 282 | HostPort: 9090 283 | Environment: 284 | - Name: WA_WEB_ENDPOINT 285 | Value: !Join 286 | - "" 287 | - - !Ref WAEntLB 288 | - ":443" 289 | - Name: WA_WEB_USERNAME 290 | Value: !Ref WAWebUsername 291 | - Name: WA_MYSQLD_EXPORTER_ENDPOINT 292 | Value: "mysqld-exporter:9104" 293 | - Name: WA_CORE_ENDPOINT 294 | Value: !Ref WAEntLB 295 | - Name: WA_NODE_EXPORTER_PORT 296 | Value: 9100 297 | - Name: WA_CADVISOR_PORT 298 | Value: 8080 299 | - Name: WA_PROMETHEUS_STORAGE_TSDB_RETENTION 300 | Value: 15d 301 | Secrets: 302 | - Name: WA_WEB_PASSWORD 303 | ValueFrom: !Sub "${WAMonitoringSecret}:wa_web_pwd::" 304 | - Name: grafana 305 | Image: !Sub "${WAEntContRegistry}/grafana:${WAEntContTag}" 306 | MemoryReservation: "1024" 307 | LogConfiguration: 308 | LogDriver: awslogs 309 | Options: 310 | awslogs-group: !Ref "CloudwatchLogsGroup" 311 | awslogs-region: !Ref "AWS::Region" 312 | awslogs-stream-prefix: grafana 313 | Links: ["prometheus"] 314 | PortMappings: 315 | - ContainerPort: 3000 316 | HostPort: 3000 317 | MountPoints: 318 | - ContainerPath: /var/lib/grafana 319 | SourceVolume: grafana-vol 320 | Environment: 321 | - Name: WA_PROMETHEUS_ENDPOINT 322 | Value: http://prometheus:9090 323 | - Name: GF_SMTP_ENABLED 324 | Value: !Ref GrafanaEnableSmtp 325 | - Name: GF_SMTP_HOST 326 | Value: !Ref GrafanaSmtpHost 327 | - Name: GF_SMTP_USER 328 | Value: !Ref GrafanaSmtpUser 329 | - Name: GF_SMTP_SKIP_VERIFY 330 | Value: 1 331 | Secrets: 332 | - Name: GF_SECURITY_ADMIN_PASSWORD 333 | ValueFrom: !Sub "${WAMonitoringSecret}:grafana_pwd::" 334 | - Name: GF_SMTP_PASSWORD 335 | ValueFrom: !Sub "${WAMonitoringSecret}:grafana_smtp_pwd::" 336 | Volumes: 337 | - Name: prom-vol 338 | Host: 339 | SourcePath: /prometheus-data 340 | - Name: grafana-vol 341 | Host: 342 | SourcePath: /var/lib/grafana 343 | 344 | WAMonitoringLB: 345 | Type: AWS::ElasticLoadBalancing::LoadBalancer 346 | Properties: 347 | ConnectionDrainingPolicy: 348 | Enabled: "true" 349 | Timeout: "15" 350 | ConnectionSettings: 351 | IdleTimeout: 30 352 | HealthCheck: 353 | Target: "TCP:3000" 354 | HealthyThreshold: "2" 355 | UnhealthyThreshold: "3" 356 | Interval: "5" # '10' 357 | Timeout: "2" 358 | LoadBalancerName: !Sub "${AWS::StackName}-LB" 359 | Listeners: 360 | - LoadBalancerPort: "3000" 361 | InstancePort: "3000" 362 | Protocol: TCP 363 | - LoadBalancerPort: "9090" 364 | InstancePort: "9090" 365 | Protocol: TCP 366 | Scheme: !Ref "LBScheme" 367 | SecurityGroups: [!Ref "EcsSecurityGroup"] 368 | Subnets: [!Ref "LBSubnet"] 369 | 370 | ECSAutoScalingGroup: 371 | Type: AWS::AutoScaling::AutoScalingGroup 372 | Properties: 373 | VPCZoneIdentifier: [!Ref "DeploymentSubnet"] 374 | LaunchConfigurationName: !Ref "ContainerInstances" 375 | MinSize: 1 376 | MaxSize: 1 377 | DesiredCapacity: 1 378 | LoadBalancerNames: [!Ref "WAMonitoringLB"] 379 | CreationPolicy: 380 | ResourceSignal: 381 | Timeout: PT15M 382 | UpdatePolicy: 383 | AutoScalingReplacingUpdate: 384 | WillReplace: "true" 385 | 386 | ContainerInstances: 387 | Type: AWS::AutoScaling::LaunchConfiguration 388 | Metadata: 389 | AWS::CloudFormation::Init: 390 | configSets: 391 | DockerConfig: 392 | - pkg_install 393 | - ecs_agent_setup 394 | pkg_install: 395 | commands: 396 | 01_update_and_prepare: 397 | command: | 398 | yum update -y 399 | yum install -y wget yum-utils 400 | yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 401 | yum makecache 402 | 02_pkg_install: 403 | command: | 404 | yum install -y docker-ce mysql epel-release python-boto3 awscli 405 | ecs_agent_setup: 406 | ## See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-agent-install.html 407 | files: 408 | /etc/ecs/ecs.config: 409 | content: | 410 | ECS_DATADIR=/data 411 | ECS_ENABLE_TASK_IAM_ROLE=true 412 | ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true 413 | ECS_LOGFILE=/log/ecs-agent.log 414 | ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","awslogs"] 415 | ECS_LOGLEVEL=info 416 | ECS_CLUSTER=default 417 | ECS_ENABLE_AWSLOGS_EXECUTIONROLE_OVERRIDE=true 418 | mode: "000644" 419 | owner: "root" 420 | group: "root" 421 | /etc/systemd/system/docker-container@ecs-agent.service: 422 | content: | 423 | [Unit] 424 | Description=Docker Container %I 425 | Requires=docker.service 426 | After=docker.service 427 | 428 | [Service] 429 | Restart=always 430 | ExecStart=/usr/bin/docker run --name %i \ 431 | --privileged \ 432 | --restart=on-failure:10 \ 433 | --volume=/var/run:/var/run \ 434 | --volume=/var/log/ecs/:/log:Z \ 435 | --volume=/var/lib/ecs/data:/data:Z \ 436 | --volume=/etc/ecs:/etc/ecs \ 437 | --net=host \ 438 | --env-file=/etc/ecs/ecs.config \ 439 | amazon/amazon-ecs-agent:latest 440 | ExecStop=/usr/bin/docker rm -f %i 441 | 442 | [Install] 443 | WantedBy=default.target 444 | mode: "000644" 445 | owner: "root" 446 | group: "root" 447 | /opt/whatsapp/bin/update_ecs_agent.sh: 448 | content: | 449 | echo "Updating ecs-agent..." 450 | systemctl stop docker-container@ecs-agent.service && docker pull amazon/amazon-ecs-agent:latest && systemctl start docker-container@ecs-agent.service 451 | mode: "000755" 452 | owner: "root" 453 | group: "root" 454 | commands: 455 | 01_ecs_related: 456 | command: | 457 | echo 'net.ipv4.conf.all.route_localnet = 1' >> /etc/sysctl.conf 458 | sysctl -p /etc/sysctl.conf 459 | iptables -t nat -A PREROUTING -p tcp -d 169.254.170.2 --dport 80 -j DNAT --to-destination 127.0.0.1:51679 460 | iptables -t nat -A OUTPUT -d 169.254.170.2 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 51679 461 | iptables-save > /etc/sysconfig/iptables 462 | 02_disable_docker_ecs: 463 | command: | 464 | systemctl disable docker 465 | systemctl disable docker-container@ecs-agent.service 466 | 03_create_log_dir: 467 | command: | 468 | mkdir -p /var/log/ecs /var/lib/ecs/data 469 | 04_weekly_auto_update: 470 | command: | 471 | # Setup a cron job to run the ecs agent update script every week. 472 | aws s3 cp s3://wa-biz-cfn-dev/scripts/update_ecs_agent.sh /opt/whatsapp/bin/update_ecs_agent.sh 473 | chmod +x /opt/whatsapp/bin/update_ecs_agent.sh 474 | crontab -l > /tmp/crontab_update_ecs.txt 475 | echo '0 0 * * 0 /opt/whatsapp/bin/update_ecs_agent.sh' >> /tmp/crontab_update_ecs.txt 476 | crontab /tmp/crontab_update_ecs.txt 477 | Properties: 478 | IamInstanceProfile: !Ref "EC2InstanceProfile" 479 | ImageId: !FindInMap [AWSRegionToAMI, !Ref "AWS::Region", AMIID] 480 | InstanceType: t2.medium 481 | KeyName: !Ref "KeyName" 482 | SecurityGroups: [!Ref "EcsSecurityGroup"] 483 | BlockDeviceMappings: 484 | - DeviceName: "/dev/sda1" 485 | Ebs: 486 | DeleteOnTermination: "true" 487 | VolumeSize: 50 488 | VolumeType: "gp2" 489 | UserData: 490 | Fn::Base64: !Sub | 491 | #!/bin/bash -ex 492 | # Setup cfn-init, cfn-signal and cfn-hup script. 493 | yum -y update 494 | yum -y install epel-release python3 python3-pip 495 | pip3 install --upgrade pip 496 | pip3 install --upgrade setuptools 497 | pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz 498 | 499 | export PATH=$PATH:/usr/local/bin 500 | 501 | # Invoke DockerConfig configset to setup docker and ecs agent. 502 | cfn-init -v --stack ${AWS::StackName} --resource ContainerInstances --configsets DockerConfig --region ${AWS::Region} 503 | result=$(($result + $?)) 504 | echo "CFN-Init (DockerConfig) status: $result" >> /var/log/whatsapp.log 505 | 506 | # Update ECS config file 507 | ## Registers this EC2 instance in the particular ECS Cluster 508 | echo "ECS_ENABLE_CONTAINER_METADATA=true" >> /etc/ecs/ecs.config 509 | echo "ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION=72h" >> /etc/ecs/ecs.config 510 | sed -i.bak 's#ECS_CLUSTER=.*#ECS_CLUSTER=${ECSCluster}#g' /etc/ecs/ecs.config 511 | systemctl enable docker-container@ecs-agent.service 512 | systemctl start docker-container@ecs-agent.service 513 | result=$(($result + $?)) 514 | echo "ECS agent service start status: $result" >> /var/log/whatsapp.log 515 | 516 | cfn-signal -e $result --stack ${AWS::StackName} --resource ECSAutoScalingGroup --region ${AWS::Region} 517 | echo "Signalled CFN with status: $result" >> /var/log/whatsapp.log 518 | 519 | echo "Successfully executed the script" >> /var/log/whatsapp.log 520 | 521 | WAMonitoringService: 522 | Type: AWS::ECS::Service 523 | DependsOn: 524 | - ECSAutoScalingGroup 525 | Properties: 526 | Cluster: !Ref "ECSCluster" 527 | DesiredCount: "1" 528 | LoadBalancers: 529 | - ContainerName: grafana 530 | ContainerPort: "3000" 531 | LoadBalancerName: !Ref "WAMonitoringLB" 532 | Role: !GetAtt ECSServiceRole.Arn 533 | TaskDefinition: !Ref "WAMonitoringTaskDefinition" 534 | DeploymentConfiguration: 535 | MaximumPercent: 100 536 | MinimumHealthyPercent: 0 537 | 538 | ECSTaskExecutionRole: 539 | Type: AWS::IAM::Role 540 | Properties: 541 | AssumeRolePolicyDocument: 542 | Version: "2012-10-17" 543 | Statement: 544 | - Effect: Allow 545 | Principal: 546 | Service: 547 | - ecs-tasks.amazonaws.com 548 | Action: 549 | - "sts:AssumeRole" 550 | Path: /whatsapp/ 551 | 552 | ECSSecretsManagerPolicy: 553 | Type: AWS::IAM::Policy 554 | Properties: 555 | PolicyName: ECSSecretsManagerPolicy 556 | PolicyDocument: 557 | Version: "2012-10-17" 558 | Statement: 559 | - Effect: Allow 560 | Action: 561 | - "ecr:GetAuthorizationToken" 562 | - "ecr:BatchCheckLayerAvailability" 563 | - "ecr:GetDownloadUrlForLayer" 564 | - "ecr:BatchGetImage" 565 | - "logs:CreateLogStream" 566 | - "logs:PutLogEvents" 567 | - "ssm:GetParameters" 568 | Resource: "*" 569 | - Effect: Allow 570 | Action: 571 | - "secretsmanager:GetSecretValue" 572 | Resource: !Ref WAMonitoringSecret 573 | Roles: [!Ref "ECSTaskExecutionRole"] 574 | 575 | ECSServiceRole: 576 | Type: AWS::IAM::Role 577 | Properties: 578 | AssumeRolePolicyDocument: 579 | Version: "2012-10-17" 580 | Statement: 581 | - Effect: Allow 582 | Principal: 583 | Service: 584 | - ecs.amazonaws.com 585 | Action: 586 | ## sts: Simple Token Service 587 | - "sts:AssumeRole" 588 | Path: /whatsapp/ 589 | 590 | ECSServicePolicy: 591 | Type: AWS::IAM::Policy 592 | Properties: 593 | PolicyName: ECSServicePolicy 594 | PolicyDocument: 595 | Version: "2012-10-17" 596 | Statement: 597 | - Effect: Allow 598 | Action: 599 | - "elasticloadbalancing:Describe*" 600 | - "elasticloadbalancing:DeregisterInstancesFromLoadBalancer" 601 | - "elasticloadbalancing:DeregisterTargets" 602 | - "elasticloadbalancing:RegisterInstancesWithLoadBalancer" 603 | - "elasticloadbalancing:RegisterTargets" 604 | - "ec2:Describe*" 605 | - "ec2:AuthorizeSecurityGroupIngress" 606 | Resource: "*" 607 | Roles: [!Ref "ECSServiceRole"] 608 | 609 | # Set up EC2 IAM role and its policies 610 | EC2Role: 611 | Type: AWS::IAM::Role 612 | Properties: 613 | AssumeRolePolicyDocument: 614 | Version: "2012-10-17" 615 | Statement: 616 | - Effect: Allow 617 | Principal: 618 | Service: 619 | - ec2.amazonaws.com 620 | Action: 621 | - "sts:AssumeRole" 622 | ManagedPolicyArns: 623 | - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess 624 | Path: /whatsapp/ 625 | 626 | ECSManagementPolicy: 627 | Type: AWS::IAM::Policy 628 | Properties: 629 | PolicyName: ECSManagementPolicy 630 | PolicyDocument: 631 | Version: "2012-10-17" 632 | Statement: 633 | - Effect: Allow 634 | Action: 635 | - "ecs:DeregisterContainerInstance" 636 | - "ecs:RegisterContainerInstance" 637 | - "ecs:Submit*" 638 | Resource: !GetAtt ECSCluster.Arn 639 | - Effect: Allow 640 | Action: 641 | - "ecs:StartTelemetrySession" 642 | - "ecs:Poll" 643 | Resource: !Sub "arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:container-instance/*" 644 | - Effect: Allow 645 | Action: 646 | - "ecs:DiscoverPollEndpoint" 647 | Resource: "*" 648 | Roles: [!Ref "EC2Role"] 649 | 650 | LogPolicy: 651 | Type: AWS::IAM::Policy 652 | Properties: 653 | PolicyName: LogPolicy 654 | PolicyDocument: 655 | Version: "2012-10-17" 656 | Statement: 657 | - Effect: Allow 658 | Action: 659 | - "logs:CreateLogGroup" 660 | - "logs:CreateLogStream" 661 | - "logs:PutLogEvents" 662 | - "logs:DescribeLogStreams" 663 | Resource: "arn:aws:logs:*:*:*" 664 | Roles: [!Ref "EC2Role"] 665 | 666 | EC2InstanceProfile: 667 | Type: AWS::IAM::InstanceProfile 668 | Properties: 669 | Path: /whatsapp/ 670 | Roles: 671 | - !Ref "EC2Role" 672 | 673 | Outputs: 674 | GrafanaDashboardURL: 675 | Description: Grafana Dashboard URL 676 | Value: !Sub "http://${WAMonitoringLB.DNSName}:3000/d/whatsapp/biz-clients" 677 | PrometheusTargetsURL: 678 | Description: Prometheus Target URL 679 | Value: !Sub "http://${WAMonitoringLB.DNSName}:9090/targets" 680 | --------------------------------------------------------------------------------