From 2869c4aa3d5c359b7bbf9010625607abe027416d Mon Sep 17 00:00:00 2001 From: Luke Rogerson Date: Fri, 15 Mar 2024 22:40:53 +0000 Subject: [PATCH] Heroku IaC --- .github/actions/terraform/action.yml | 109 +++++++++++++++ .github/actions/terraform/box.sh | 4 + cmd/order-book/main.go | 12 +- infrastructure/.terraform.lock.hcl | 90 +++++------- infrastructure/environments/dev/dev.tfvars | 4 + infrastructure/environments/prod/prod.tfvars | 5 +- .../environments/staging/staging.tfvars | 2 + infrastructure/locals.tf | 7 + infrastructure/main.tf | 131 ++++++++++++++++++ infrastructure/providers.tf | 7 +- infrastructure/variables.tf | 48 +++---- 11 files changed, 330 insertions(+), 89 deletions(-) create mode 100644 .github/actions/terraform/action.yml create mode 100644 .github/actions/terraform/box.sh create mode 100644 infrastructure/environments/dev/dev.tfvars create mode 100644 infrastructure/environments/staging/staging.tfvars create mode 100644 infrastructure/locals.tf create mode 100644 infrastructure/main.tf diff --git a/.github/actions/terraform/action.yml b/.github/actions/terraform/action.yml new file mode 100644 index 0000000..5eb4f9e --- /dev/null +++ b/.github/actions/terraform/action.yml @@ -0,0 +1,109 @@ +name: AWS Terraform +description: Runs Terraform against AWS +inputs: + terraform_state_s3_bucket: + description: "S3 bucket for Terraform state" + required: true + terraform_state_s3_key_prefix: + description: "S3 key prefix for Terraform state" + required: true + terraform_state_dynamodb_table: + description: "DynamoDB table for Terraform state" + required: true + build_path: + description: "Build path that contains the source" + required: true + environment: + description: "Name of the environemnt e.g. dev, staging, prod" + required: true + branch: + description: "Git branch being ran against" + required: true + image_tag: + description: "Docker image tag to deploy" + required: false + +runs: + using: "composite" + steps: + - name: Check permissions + id: permissions + run: | + chmod 777 *.sh + chmod +x *.sh + echo -e "\n" + shell: bash + working-directory: "${{ github.action_path }}" + + - name: Setup build environment + id: setup + run: | + ${{ github.action_path }}/box.sh "Setting up build environment" + echo -e "Installing TFLint" + curl -L "$(curl -Ls https://api.github.com/repos/terraform-linters/tflint/releases/latest | grep -o -E "https://.+?_linux_amd64.zip")" -o tflint.zip && unzip tflint.zip && rm tflint.zip + curl -L "$(curl -Ls https://api.github.com/repos/terraform-linters/tflint-ruleset-aws/releases/latest | grep -o -E "https://.+?_linux_amd64.zip")" -o tflint-ruleset.zip && unzip tflint-ruleset.zip && rm tflint-ruleset.zip + mkdir -p ./.tflint.d/plugins/ + mv tflint-ruleset-aws ./.tflint.d/plugins/ + echo -e "\n" + shell: bash + working-directory: "${{ inputs.build_path }}/infrastructure" + + - name: Terraform Init + id: init + run: | + ${{ github.action_path }}/box.sh "Running Terraform init" + terraform init -backend-config="region=eu-west-1" -backend-config="dynamodb_table=${{ inputs.terraform_state_dynamodb_table }}" -backend-config="encrypt=true" -backend-config="workspace_key_prefix=${{ inputs.terraform_state_s3_key_prefix }}" -backend-config="bucket=${{ inputs.terraform_state_s3_bucket }}" -backend-config="key=${{ inputs.terraform_state_s3_key_prefix }}/${{ inputs.environment }}/terraform.tfstate" + echo -e "Terraform S3 bucket: ${{ inputs.terraform_state_s3_bucket }}" + echo -e "Terraform state file: ${{ inputs.terraform_state_s3_key_prefix }}/${{ inputs.environment }}/terraform.tfstate" + echo -e "\n" + shell: bash + working-directory: "${{ inputs.build_path }}/infrastructure" + + - name: Terraform Validate + id: validate + run: | + ${{ github.action_path }}/box.sh "Running Terraform validate" + terraform validate + echo -e "\n" + shell: bash + working-directory: "${{ inputs.build_path }}/infrastructure" + + - name: TFLint + id: lint + run: | + ${{ github.action_path }}/box.sh "Running TFLint" + ./tflint --init + export TFLINT_LOG=info + ./tflint --var-file='./environments/${{ inputs.environment }}/${{ inputs.environment }}.tfvars' --module --config=./.tflint.hcl || true + # echo -e "\n" + shell: bash + working-directory: "${{ inputs.build_path }}/infrastructure" + + - name: Terraform Plan + id: plan + run: | + export TF_VAR_environment=${environment} + ${{ github.action_path }}/box.sh "Running Terraform plan" + plan_command="terraform plan -var-file='./environments/${{ inputs.environment }}/${{ inputs.environment }}.tfvars' -var 'region=${{ inputs.aws_deploy_region }}' -var 'image_tag=${{ inputs.image_tag }}' -input=false -out=plan.out" + if [ "${{ inputs.branch }}" == "main" ]; then + eval $plan_command + else + plan_command+=" -lock=false" + eval $plan_command + fi + echo -e "\n" + shell: bash + working-directory: "${{ inputs.build_path }}/infrastructure" + + - name: Terraform Apply + id: apply + run: | + if [ "${{ inputs.branch }}" == "main" ]; then + ${{ github.action_path }}/box.sh "Running Terraform apply" + terraform apply -auto-approve -input=false plan.out + else + echo -e "Not on dev, staging or main branch, so skipping Terraform apply." + fi + echo -e "Terraform run completed successfully." + shell: bash + working-directory: "${{ inputs.build_path }}/infrastructure" diff --git a/.github/actions/terraform/box.sh b/.github/actions/terraform/box.sh new file mode 100644 index 0000000..c020587 --- /dev/null +++ b/.github/actions/terraform/box.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +echo $1 | sed -e 's/^/../' -e 's/$/../' -e 's/./*/g' +echo $1 | sed -e 's/^/* /' -e 's/$/ */' +echo $1 | sed -e 's/^/../' -e 's/$/../' -e 's/./*/g' diff --git a/cmd/order-book/main.go b/cmd/order-book/main.go index 38bccb2..64afdd5 100644 --- a/cmd/order-book/main.go +++ b/cmd/order-book/main.go @@ -18,24 +18,22 @@ import ( "github.com/orbs-network/order-book/transport/rest" ) -// TODO: handle build version better -const VERSION = "1.0.1" - func main() { setup() } func setup() { - log.Print("Order book version: ", VERSION) - redisAddress, found := os.LookupEnv("REDIS_URL") if !found { - panic("REDIS_URL not set") + redisAddress, found = os.LookupEnv("REDISCLOUD_URL") + if !found { + panic("Neither REDIS_URL nor REDISCLOUD_URL is set") + } } opt, err := redis.ParseURL(redisAddress) if err != nil { - panic(fmt.Errorf("failed to parse redis url: %v", err)) + panic(fmt.Errorf("failed to parse redis URL: %v", err)) } log.Printf("Redis address: %s", opt.Addr) diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl index 635b13d..cd888d4 100644 --- a/infrastructure/.terraform.lock.hcl +++ b/infrastructure/.terraform.lock.hcl @@ -2,64 +2,48 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "5.39.1" - constraints = ">= 2.0.0, >= 3.0.0, >= 3.71.0, >= 4.9.0, >= 4.18.0, ~> 5.0" + version = "5.40.0" + constraints = "~> 5.0" hashes = [ - "h1:V29aKJqUWugn9F2pBIaWLwHLTmgFpirKEv1aNRX1kV4=", - "zh:05c50a5d8edb3ba4ebc4eb6e0d0b5e319142f5983b27821710ed7d475d335bdc", - "zh:082986a5784dd21957e632371b289e549f051a4ea21d5c78c6d744c3537f03c5", - "zh:192ae622ba562eacc4921ed549a794506179233d724fdd15a4f147f3400724a0", - "zh:19a1d4637a62de90b0da174c0bf01000cd900488f7e8f709d8a37f082c59756b", - "zh:1d7689a8583515f1705972d7ce57ccfab96215b19905530d2c78c02dcfaff583", - "zh:22c446a21209a52ab74b4ba1ede0b220531e97ce479430047e493a2c45e1d8cb", - "zh:4154de82290ab4e9f81bac1ea62342de8b3b7a608f99258c190d4dd1c6663e47", - "zh:6bc4859ccdc54f28af9286b2fa090a31dcb345138d68c471510b737f6a052011", - "zh:73c69e000e0b321e78a4a12fef60d37285f2afec0ea7be9e06163d985101cb59", - "zh:890a3422f5e445b49bae30facf448d0ec9cd647e9155d0b685b5b39e9d331a94", + "h1:KEqMoJwLw6Z9bTO4K8nPVvQQa6YiM+bvz89Sw7tNFJw=", + "zh:11f177a2385703740bd26d0652d3dba08575101d7639f386ce5637bdb0e29a13", + "zh:203fc43e69634f1bd487a9dc24b01944dfd568beac78e491f26677d103d343ed", + "zh:3697ebad4929da30ea98276a85d4ce5ebfc48508f4dd149e17e1dcdc7f306c6e", + "zh:421e0799756587e728f75a9024b8d4e38707cd6d65cf0710cb8d189062c85a58", + "zh:4be2adcd4c32a66159c532908f0d425d793c814b3686832e9af549b1515ae032", + "zh:55778b32470212ce6bbfd402529c88e7ea6ba34b0882f85d6ea001ff5c6255a5", + "zh:689a4c1fd1e1d5dab7b169759389c76f25e366f19a470971674321d6fca09791", + "zh:68a23eda608573a053e8738894457bd0c11766bc243e68826c78ab6b5a144710", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9cd88bec0f5205df9032e3126d4e57edd1c5cc8d45cda25626882dafc485a3b0", - "zh:a3a8e3276d0fbf051bbafa192a2998b05745f2cf285ac8c36a9ad167a75c037f", - "zh:d47e4dcf4c0ad71b9a7c720be4f3a89f6786a82e77bbe8d950794562792a1da5", - "zh:f74e5b2af508c7de80a6ae5198df54a795eeba5058a0cd247828943f0c54f6e0", + "zh:a1580115c22564e5752e569dc40482503de6cced44da3e9431885cd9d4bf18ea", + "zh:b127756d7ee513691e76c211570580c10eaa2f7a7e4fd27c3566a48ec214991c", + "zh:b7ccea7a759940c8dcf8726272eed6653eed0b31f7223f71e829a344627afd39", + "zh:bb130fc50494fd45406e04b44d242da9a8f138a4a43feb65cf9e86d13aa13629", + "zh:cf1c972c90d5f22c9705274a33792275e284a0a3fcac12ce4083b5a4480463f4", + "zh:ebe60d3887b23703ca6a4c65b15c6d7b8d93ba27a028d996d17882fe6e98d5c0", ] } -provider "registry.terraform.io/hashicorp/null" { - version = "3.2.2" - constraints = ">= 3.0.0" +provider "registry.terraform.io/heroku/heroku" { + version = "5.2.8" + constraints = "~> 5.0" hashes = [ - "h1:IMVAUHKoydFrlPrl9OzasDnw/8ntZFerCC9iXw1rXQY=", - "zh:3248aae6a2198f3ec8394218d05bd5e42be59f43a3a7c0b71c66ec0df08b69e7", - "zh:32b1aaa1c3013d33c245493f4a65465eab9436b454d250102729321a44c8ab9a", - "zh:38eff7e470acb48f66380a73a5c7cdd76cc9b9c9ba9a7249c7991488abe22fe3", - "zh:4c2f1faee67af104f5f9e711c4574ff4d298afaa8a420680b0cb55d7bbc65606", - "zh:544b33b757c0b954dbb87db83a5ad921edd61f02f1dc86c6186a5ea86465b546", - "zh:696cf785090e1e8cf1587499516b0494f47413b43cb99877ad97f5d0de3dc539", - "zh:6e301f34757b5d265ae44467d95306d61bef5e41930be1365f5a8dcf80f59452", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:913a929070c819e59e94bb37a2a253c228f83921136ff4a7aa1a178c7cce5422", - "zh:aa9015926cd152425dbf86d1abdbc74bfe0e1ba3d26b3db35051d7b9ca9f72ae", - "zh:bb04798b016e1e1d49bcc76d62c53b56c88c63d6f2dfe38821afef17c416a0e1", - "zh:c23084e1b23577de22603cff752e59128d83cfecc2e6819edadd8cf7a10af11e", - ] -} - -provider "registry.terraform.io/hashicorp/random" { - version = "3.6.0" - constraints = ">= 3.0.0" - hashes = [ - "h1:I8MBeauYA8J8yheLJ8oSMWqB0kovn16dF/wKZ1QTdkk=", - "zh:03360ed3ecd31e8c5dac9c95fe0858be50f3e9a0d0c654b5e504109c2159287d", - "zh:1c67ac51254ba2a2bb53a25e8ae7e4d076103483f55f39b426ec55e47d1fe211", - "zh:24a17bba7f6d679538ff51b3a2f378cedadede97af8a1db7dad4fd8d6d50f829", - "zh:30ffb297ffd1633175d6545d37c2217e2cef9545a6e03946e514c59c0859b77d", - "zh:454ce4b3dbc73e6775f2f6605d45cee6e16c3872a2e66a2c97993d6e5cbd7055", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:91df0a9fab329aff2ff4cf26797592eb7a3a90b4a0c04d64ce186654e0cc6e17", - "zh:aa57384b85622a9f7bfb5d4512ca88e61f22a9cea9f30febaa4c98c68ff0dc21", - "zh:c4a3e329ba786ffb6f2b694e1fd41d413a7010f3a53c20b432325a94fa71e839", - "zh:e2699bc9116447f96c53d55f2a00570f982e6f9935038c3810603572693712d0", - "zh:e747c0fd5d7684e5bfad8aa0ca441903f15ae7a98a737ff6aca24ba223207e2c", - "zh:f1ca75f417ce490368f047b63ec09fd003711ae48487fba90b4aba2ccf71920e", + "h1:vwO0uYdJ8XtncSkNdk6o5H/7b1zO/nu12IwazJH0slQ=", + "zh:0aca7d8e57bb8c23e7a7e122408f1c46375f84671047f281562b1df76fec850f", + "zh:36a6f9bf29c5909f963a393bb1f3ea86ef7f6743205d4969b11e54e8f325b88a", + "zh:380599c1da3ffe74967dbe60d140900b405b5d4d3ac81ae52223a17a926ab9a5", + "zh:5dd595964d8bce76d08e44ff65a140da358f4bfea53d878640c5ca58147e127a", + "zh:70f2a7441913e5511c4ee7973b61c390b8befa4ddb6bfbac97b8eb08cfd07efe", + "zh:760fb47960ed13b81e4f281ef6cd10d691071f3f45711b4885b192beb3d237a4", + "zh:7b3785b69ab01bf76fb5feaa0fa806bd4671b4f15e7e7a0801b40730c086ef0e", + "zh:85c4a6db7d06ccd6abbf858da15235b75ad8fdcaf3b2a4e1c9e171a9a6ab627f", + "zh:918635702ae5e33c984b73096d42760a661a9c5670ae94500fa2391f8a409aec", + "zh:a542dda7daec593892233b13091e73c31167004481b951f2968d8adda6337b6c", + "zh:a783f3a6f24b483f0c249a6049c13bbd66db55861d18727f085330f8552efb0c", + "zh:b785f6102ec932fc3a588abbf9689f7a7020fc0966fce3ae4fa75c9ec2f8f39c", + "zh:d04ff26791eb021fbf08a37136f4a346ef56394d12b90a6c8f3d48beef40c1cb", + "zh:d3292f21c54d93dca27a4f6f06a8866e5a97093f839bade2ced8ace9bcd366f1", + "zh:e37a869305afb36c40f5b5f7898b52b8c8fd84f97002773fdc3cc24b97b15ada", + "zh:ed85f8914dc688c9768cc6436b48fb420b5eb950b10e8cba26ed209f5049f191", ] } diff --git a/infrastructure/environments/dev/dev.tfvars b/infrastructure/environments/dev/dev.tfvars new file mode 100644 index 0000000..f7cfae4 --- /dev/null +++ b/infrastructure/environments/dev/dev.tfvars @@ -0,0 +1,4 @@ +environment_name = "development" +log_level = "info" +maker_mock_api_key = "test" +maker_mock_private_key = "0xtest" diff --git a/infrastructure/environments/prod/prod.tfvars b/infrastructure/environments/prod/prod.tfvars index 353c562..954aa1e 100644 --- a/infrastructure/environments/prod/prod.tfvars +++ b/infrastructure/environments/prod/prod.tfvars @@ -1,3 +1,2 @@ -environment_name = "prod" -image_tag = "0.0.1" -az_count = 1 +environment_name = "production" +log_level = "info" diff --git a/infrastructure/environments/staging/staging.tfvars b/infrastructure/environments/staging/staging.tfvars new file mode 100644 index 0000000..650ee98 --- /dev/null +++ b/infrastructure/environments/staging/staging.tfvars @@ -0,0 +1,2 @@ +environment_name = "staging" +log_level = "info" diff --git a/infrastructure/locals.tf b/infrastructure/locals.tf new file mode 100644 index 0000000..e051187 --- /dev/null +++ b/infrastructure/locals.tf @@ -0,0 +1,7 @@ +locals { + redis_plan = { + development = "rediscloud" + staging = "rediscloud:100" + production = "rediscloud:500" + } +} diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 0000000..9010f4a --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,131 @@ +data "heroku_pipeline" "pipeline" { + name = "order-book" +} + +# --- Server --- +resource "heroku_app" "server" { + name = "ob-server-${var.environment_name}" + region = "us" + + organization { + name = "orbs" + locked = true + } + + config_vars = { + "ENVIRONMENT" = var.environment_name + "LOG_LEVEL" = var.log_level + "RPC_URL" = var.rpc_url + "SWAP_CONTRACT_ADDRESS" = var.swap_contract_address + "VERIFY_SIGNATURE" = var.verify_signature + } + + lifecycle { + ignore_changes = [ + config_vars + ] + } +} + +resource "heroku_pipeline_coupling" "server" { + app_id = heroku_app.server.id + pipeline = data.heroku_pipeline.pipeline.id + stage = var.environment_name +} + +resource "heroku_formation" "server" { + app_id = heroku_app.server.id + type = "web" + quantity = var.environment_name == "development" ? 1 : 2 + size = var.environment_name == "development" ? "Basic" : "Standard-2X" +} + +# --- Swaps Tracker --- +resource "heroku_app" "swaps-tracker" { + name = "ob-swaps-tracker-${var.environment_name}" + region = "us" + + organization { + name = "orbs" + locked = true + } + + config_vars = { + "ENVIRONMENT" = var.environment_name + "LOG_LEVEL" = var.log_level + "RPC_URL" = var.rpc_url + } + + lifecycle { + ignore_changes = [ + config_vars + ] + } +} + +resource "heroku_pipeline_coupling" "swaps-tracker" { + app_id = heroku_app.swaps-tracker.id + pipeline = data.heroku_pipeline.pipeline.id + stage = var.environment_name +} + +resource "heroku_formation" "swaps-tracker" { + app_id = heroku_app.swaps-tracker.id + type = "worker" + quantity = 1 + size = var.environment_name == "development" ? "Basic" : "Standard-2X" +} + +# --- Maker Mock --- +resource "heroku_app" "maker-mock" { + count = var.environment_name == "development" ? 1 : 0 + name = "ob-maker-mock" + region = "us" + + organization { + name = "orbs" + locked = true + } + + config_vars = { + "ENVIRONMENT" = var.environment_name + "LOG_LEVEL" = var.log_level + "API_KEY" = var.maker_mock_api_key + "BASE_URL" = heroku_app.server.web_url + "IS_DISABLED" = var.maker_mock_is_disabled + "PRIVATE_KEY" = var.maker_mock_private_key + } + + lifecycle { + ignore_changes = [ + config_vars + ] + } +} + +resource "heroku_pipeline_coupling" "maker-mock" { + count = var.environment_name == "development" ? 1 : 0 + app_id = heroku_app.maker-mock[0].id + pipeline = data.heroku_pipeline.pipeline.id + stage = var.environment_name +} + +resource "heroku_formation" "maker-mock" { + count = var.environment_name == "development" ? 1 : 0 + app_id = heroku_app.maker-mock[0].id + type = "worker" + quantity = 1 + size = var.environment_name == "development" ? "Standard-1X" : "Standard-2X" +} + +# --- Redis --- +# https://elements.heroku.com/addons/rediscloud +resource "heroku_addon" "redis" { + app_id = heroku_app.server.id + plan = local.redis_plan[var.environment_name] +} + +resource "heroku_addon_attachment" "redis" { + app_id = heroku_app.swaps-tracker.id + addon_id = heroku_addon.redis.id +} diff --git a/infrastructure/providers.tf b/infrastructure/providers.tf index 8f020bd..4acc40e 100644 --- a/infrastructure/providers.tf +++ b/infrastructure/providers.tf @@ -4,6 +4,10 @@ terraform { source = "hashicorp/aws" version = "~> 5.0" } + heroku = { + source = "heroku/heroku" + version = "~> 5.0" + } } @@ -11,9 +15,8 @@ terraform { } provider "aws" { - region = var.region assume_role { - role_arn = "arn:aws:iam::${var.aws_deploy_account}:role/${var.aws_deploy_iam_role_name}" + role_arn = "arn:aws:iam::506367651493:role/terraform" session_name = "Terraform" } } diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf index 1642914..2823d77 100644 --- a/infrastructure/variables.tf +++ b/infrastructure/variables.tf @@ -1,45 +1,45 @@ -variable "aws_deploy_account" { - description = "AWS account id to deploy to" +variable "environment_name" { type = string - default = "506367651493" + description = "Environment specific name" } -variable "aws_deploy_iam_role_name" { - description = "AWS IAM role name to assume for deployment" +variable "rpc_url" { type = string - default = "terraform" + description = "Blockchain RPC URL for the application" + sensitive = true } -variable "environment_name" { +variable "log_level" { type = string - description = "Environment specific name" + description = "Log level for the application" + default = "info" } -variable "region" { +variable "swap_contract_address" { type = string - default = "ap-northeast-1" - description = "AWS region used" + description = "Swap reactor contract address" } -variable "image_tag" { +variable "verify_signature" { type = string - description = "Docker image tag" + description = "Whether to verify the signature sent with the order (not compulsory as the signature is verified on-chain)" + default = "false" } -variable "vpc_cidr_block" { +variable "maker_mock_api_key" { type = string - description = "The CIDR block for the VPC" - default = "10.1.0.0/16" + description = "Heroku API key" + default = "test" } -variable "multi_az_enabled" { - type = bool - description = "Enable multi AZ redundancy" - default = false +variable "maker_mock_is_disabled" { + type = string + description = "Whether to disable the maker mock creating new orders" + default = "true" } -variable "az_count" { - type = number - description = "Number of availability zones to use" - default = 3 +variable "maker_mock_private_key" { + type = string + description = "Private key for the maker mock" + default = "test" }