diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d87e8c3..a34798a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -68,7 +68,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: "🔨 Prebuild docker dependencies" + - name: "🔨 Build Ponder docker dependencies" uses: docker/build-push-action@v6 with: context: ./packages/ponder @@ -81,6 +81,19 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + - name: "🔨 Build ERPC docker dependencies" + uses: docker/build-push-action@v6 + with: + context: ./packages/erpc + platforms: linux/arm64 + push: true + tags: | + 262732185023.dkr.ecr.eu-west-1.amazonaws.com/erpc:latest + 262732185023.dkr.ecr.eu-west-1.amazonaws.com/erpc:${{ github.sha }} + # Github actions cache + cache-from: type=gha + cache-to: type=gha,mode=max + - name: "🚀 SST Deploy" run: | echo "Deploying with stage: prod" diff --git a/iac/Config.ts b/iac/Config.ts new file mode 100644 index 0000000..209458a --- /dev/null +++ b/iac/Config.ts @@ -0,0 +1,27 @@ +import { Config, type StackContext } from "sst/constructs"; + +/** + * Simple stack for the config + * @param stack + * @constructor + */ +export function ConfigStack({ stack }: StackContext) { + // RPCs + const rpcSecrets = [ + // BlockPi rpcs + new Config.Secret(stack, "BLOCKPI_API_KEY_ARB_SEPOLIA"), + // Alchemy RPC + new Config.Secret(stack, "ALCHEMY_API_KEY"), + ]; + + // Databases + const ponderDb = new Config.Secret(stack, "DATABASE_URL"); + const erpcDb = new Config.Secret(stack, "ERPC_DATABASE_URL"); + + // Return all of that + return { + rpcSecrets, + ponderDb, + erpcDb, + }; +} diff --git a/iac/Erpc.ts b/iac/Erpc.ts new file mode 100644 index 0000000..082a7c3 --- /dev/null +++ b/iac/Erpc.ts @@ -0,0 +1,90 @@ +import { Repository } from "aws-cdk-lib/aws-ecr"; +import { ContainerImage } from "aws-cdk-lib/aws-ecs"; +import { Service, type StackContext, use } from "sst/constructs"; +import { ConfigStack } from "./Config"; +import { buildSecretsMap } from "./utils"; + +/** + * The CDK stack that will deploy the erpc service + * @param stack + * @constructor + */ +export function ErpcStack({ app, stack }: StackContext) { + // All the secrets env variable we will be using (in local you can just use a .env file) + const { rpcSecrets, erpcDb } = use(ConfigStack); + const secrets = [...rpcSecrets, erpcDb]; + + // Get our CDK secrets map + const cdkSecretsMap = buildSecretsMap(stack, secrets); + + // Get the container props of our prebuilt binaries + const containerRegistry = Repository.fromRepositoryAttributes( + stack, + "ErpcEcr", + { + repositoryArn: `arn:aws:ecr:eu-west-1:${app.account}:repository/erpc`, + repositoryName: "erpc", + } + ); + + const imageTag = process.env.COMMIT_SHA ?? "latest"; + console.log(`Will use the image ${imageTag}`); + const erpcImage = ContainerImage.fromEcrRepository( + containerRegistry, + imageTag + ); + + // The service itself + const erpcService = new Service(stack, "ErpcService", { + path: "packages/erpc", + port: 4000, + // Domain mapping + customDomain: { + domainName: "erpc.frak.id", + hostedZone: "frak.id", + }, + // Setup some capacity options + scaling: { + minContainers: 1, + maxContainers: 1, + cpuUtilization: 90, + memoryUtilization: 90, + }, + // Bind the secret we will be using + bind: secrets, + // Arm architecture (lower cost) + architecture: "arm64", + // Hardware config + cpu: "1 vCPU", + memory: "4 GB", + storage: "30 GB", + // Log retention + logRetention: "one_week", + // Set the right environment variables + environment: { + ERPC_LOG_LEVEL: "warn", + }, + cdk: { + // Customise fargate service to enable circuit breaker (if the new deployment is failing) + fargateService: { + circuitBreaker: { + enable: true, + }, + // Disable rolling update + desiredCount: 1, + minHealthyPercent: 0, + maxHealthyPercent: 100, + }, + // Directly specify the image position in the registry here + container: { + containerName: "erpc", + image: erpcImage, + secrets: cdkSecretsMap, + }, + }, + }); + + stack.addOutputs({ + erpcServiceId: erpcService.id, + }); +} diff --git a/iac/Indexer.ts b/iac/Indexer.ts index 54d01b9..5cf48fc 100644 --- a/iac/Indexer.ts +++ b/iac/Indexer.ts @@ -1,7 +1,8 @@ import { Repository } from "aws-cdk-lib/aws-ecr"; -import { ContainerImage, Secret } from "aws-cdk-lib/aws-ecs"; -import { StringParameter } from "aws-cdk-lib/aws-ssm"; -import { Config, Service, type Stack, type StackContext } from "sst/constructs"; +import { ContainerImage } from "aws-cdk-lib/aws-ecs"; +import { Service, type StackContext, use } from "sst/constructs"; +import { ConfigStack } from "./Config"; +import { buildSecretsMap } from "./utils"; /** * The CDK stack that will deploy the indexer service @@ -10,14 +11,8 @@ import { Config, Service, type Stack, type StackContext } from "sst/constructs"; */ export function IndexerStack({ app, stack }: StackContext) { // All the secrets env variable we will be using (in local you can just use a .env file) - const secrets = [ - // Db url - new Config.Secret(stack, "DATABASE_URL"), - // BlockPi rpcs - new Config.Secret(stack, "BLOCKPI_API_KEY_ARB_SEPOLIA"), - // Alchemy RPC - new Config.Secret(stack, "ALCHEMY_API_KEY"), - ]; + const { rpcSecrets, ponderDb } = use(ConfigStack); + const secrets = [...rpcSecrets, ponderDb]; // Get our CDK secrets map const cdkSecretsMap = buildSecretsMap(stack, secrets); @@ -96,45 +91,4 @@ export function IndexerStack({ app, stack }: StackContext) { stack.addOutputs({ indexerServiceId: indexerService.id, }); - - // Set up connections to database via the security group - /*const cluster = indexerService.cdk?.cluster; - if (cluster) { - // Get the security group for the database and link to it - const databaseSecurityGroup = SecurityGroup.fromLookupById( - stack, - "indexer-db-sg", - "sg-0cbbb98322234113f" - ); - databaseSecurityGroup.connections.allowFrom(cluster, Port.tcp(5432)); - }*/ -} - -/** - * Build a list of secret name to CDK secret, for direct binding - * @param stack - * @param secrets - */ -function buildSecretsMap(stack: Stack, secrets: Config.Secret[]) { - return secrets.reduce( - (acc, secret) => { - const isSpecificSecret = secret.name === "DATABASE_URL"; - const ssmPath = isSpecificSecret - ? `/indexer/sst/Secret/${secret.name}/value` - : `/sst/frak-indexer/.fallback/Secret/${secret.name}/value`; - - // Add the secret - const stringParameter = - StringParameter.fromSecureStringParameterAttributes( - stack, - `Secret${secret.name}`, - { - parameterName: ssmPath, - } - ); - acc[secret.name] = Secret.fromSsmParameter(stringParameter); - return acc; - }, - {} as Record - ); } diff --git a/iac/utils.ts b/iac/utils.ts new file mode 100644 index 0000000..3e98be2 --- /dev/null +++ b/iac/utils.ts @@ -0,0 +1,34 @@ +import { Secret } from "aws-cdk-lib/aws-ecs"; +import { StringParameter } from "aws-cdk-lib/aws-ssm"; +import type { Config, Stack } from "sst/constructs"; + +const specificSecretsList = ["ERPC_DATABASE_URL", "DATABASE_URL"]; + +/** + * Build a list of secret name to CDK secret, for direct binding + * @param stack + * @param secrets + */ +export function buildSecretsMap(stack: Stack, secrets: Config.Secret[]) { + return secrets.reduce( + (acc, secret) => { + const isSpecificSecret = specificSecretsList.includes(secret.name); + const ssmPath = isSpecificSecret + ? `/indexer/sst/Secret/${secret.name}/value` + : `/sst/frak-indexer/.fallback/Secret/${secret.name}/value`; + + // Add the secret + const stringParameter = + StringParameter.fromSecureStringParameterAttributes( + stack, + `Secret${secret.name}`, + { + parameterName: ssmPath, + } + ); + acc[secret.name] = Secret.fromSsmParameter(stringParameter); + return acc; + }, + {} as Record + ); +} diff --git a/packages/erpc/erpc.yaml b/packages/erpc/erpc.yaml index 95e6922..2c01445 100644 --- a/packages/erpc/erpc.yaml +++ b/packages/erpc/erpc.yaml @@ -1,4 +1,4 @@ -logLevel: warn +logLevel: ${ERPC_LOG_LEVEL} database: # todo: Should use the same postgres as the indexer but on a different schema evmJsonRpcCache: @@ -51,7 +51,7 @@ projects: upstreams: - id: alchemy-multi-chain endpoint: alchemy://${ALCHEMY_API_KEY} - rateLimitBudget: global + rateLimitBudget: alchemy healthCheckGroup: default-hcg allowMethods: - "alchemy_*" @@ -68,15 +68,15 @@ projects: jitter: 500ms rateLimiters: budgets: - - id: default-budget + - id: alchemy rules: - method: "*" maxCount: 200 period: 1s healthChecks: groups: - - id: default-hcg - checkInterval: 300s + - id: slow-hcg + checkInterval: 600s maxErrorRatePercent: 10 maxP90LatencyMs: 5s maxBlocksBehind: 5 \ No newline at end of file diff --git a/packages/ponder/src/stats.ts b/packages/ponder/src/stats.ts index a782e2a..23ca018 100644 --- a/packages/ponder/src/stats.ts +++ b/packages/ponder/src/stats.ts @@ -51,6 +51,10 @@ export async function increaseCampaignsInteractions({ // Perform the increments // todo: Should use an `updateMany` if we are sure that campaign stats are created for (const campaign of campaigns.items) { + if (!campaign.id) { + console.error("Campaign id not found", campaign); + continue; + } // Create the stats if not found await PressCampaignStats.upsert({ id: campaign.id, diff --git a/sst.config.ts b/sst.config.ts index db9a1a4..2a4806f 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -1,4 +1,6 @@ import type { SSTConfig } from "sst"; +import { ConfigStack } from "./iac/Config"; +import { ErpcStack } from "./iac/Erpc"; import { IndexerStack } from "./iac/Indexer"; export default { @@ -26,6 +28,8 @@ export default { tracing: "disabled", }); + app.stack(ConfigStack); + app.stack(ErpcStack); app.stack(IndexerStack); }, } satisfies SSTConfig;