Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions .github/workflows/dev-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ permissions:
contents: read

jobs:
deploy_commit:
deploy-proxy:
runs-on: ubuntu-latest

environment: development
steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- uses: actions/checkout@v4.2.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-east-1
role-to-assume: arn:aws:iam::417712557820:role/GithubDeployRole
role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
Expand All @@ -46,3 +46,30 @@ jobs:
task-definition: ${{ steps.render-data-proxy-container.outputs.task-definition }}
service: source-data-proxy
cluster: SourceCooperative-Dev

deploy-cdk:
runs-on: ubuntu-latest
environment: development
defaults:
run:
working-directory: ./deploy
steps:
- uses: actions/checkout@v4.2.2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ vars.AWS_REGION }}
role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }}
- uses: actions/setup-node@v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: ./deploy/package-lock.json
- run: npm install -g aws-cdk
- run: npm ci
- name: Set vars to env
env:
VARS_JSON: ${{ toJSON(vars) }}
run: |
echo "$VARS_JSON" | jq -r 'keys[] as $k | "\($k)=\(.[$k])"' >> $GITHUB_ENV
- run: cdk deploy --all --require-approval never
35 changes: 32 additions & 3 deletions .github/workflows/prod-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ permissions:
contents: read

jobs:
deploy_release:
deploy-proxy:
runs-on: ubuntu-latest

environment: production
steps:
- id: latest
uses: thebritican/fetch-latest-release@a36ee8ee464da77ba3e499ed6b75e3530e10f9bc # v2.0.0
Expand All @@ -27,7 +27,7 @@ jobs:
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-west-2
role-to-assume: arn:aws:iam::417712557820:role/PublishECRImages
role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
Expand All @@ -51,3 +51,32 @@ jobs:
task-definition: ${{ steps.render-data-proxy-container.outputs.task-definition }}
service: source-data-proxy
cluster: SourceCooperative-Prod

deploy-cdk:
runs-on: ubuntu-latest
environment: production
defaults:
run:
working-directory: ./deploy
steps:
- uses: actions/checkout@v4.2.2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ vars.AWS_REGION }}
role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }}
- uses: actions/setup-node@v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: ./deploy/package-lock.json
- run: npm install -g aws-cdk
- run: npm ci
- name: Set vars to env
env:
VARS_JSON: ${{ toJSON(vars) }}
run: |
echo "$VARS_JSON" | jq -r 'keys[] as $k | "\($k)=\(.[$k])"' >> $GITHUB_ENV
- run: cdk deploy --all --require-approval never
env:
STAGE: prod
8 changes: 8 additions & 0 deletions deploy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions deploy/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
1 change: 1 addition & 0 deletions deploy/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
5 changes: 5 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Deployment

This directory contains deployment tooling to create and manage the AWS infrastructure for the Source Data Proxy.

It embraces an "Infrastructure as Code" approach via [AWS CDK](https://docs.aws.amazon.com/cdk/). Deployments should be triggered via Github Actions.
20 changes: 20 additions & 0 deletions deploy/bin/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import { DataProxyStack } from "../lib/data-proxy-stack";

const stage = process.env.STAGE || "dev";

const vpcId = process.env.VPC_ID;
if (!vpcId) {
throw new Error("VPC_ID is not set");
}

const app = new cdk.App();
new DataProxyStack(app, `DataProxy-${stage}`, {
vpcId,
proxyDomain: `vercel-api-${stage}.internal`,
env: {
account: process.env.AWS_ACCOUNT_ID,
region: process.env.AWS_REGION,
},
});
99 changes: 99 additions & 0 deletions deploy/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"app": "npx ts-node --prefer-ts-exts bin/deploy.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
"@aws-cdk/core:explicitStackTags": true,
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
"@aws-cdk/core:enableAdditionalMetadataCollection": true,
"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false,
"@aws-cdk/aws-s3:setUniqueReplicationRoleName": true,
"@aws-cdk/aws-events:requireEventBusPolicySid": true,
"@aws-cdk/core:aspectPrioritiesMutating": true,
"@aws-cdk/aws-dynamodb:retainTableReplica": true,
"@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true,
"@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true,
"@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true,
"@aws-cdk/aws-s3:publicAccessBlockedByDefault": true,
"@aws-cdk/aws-lambda:useCdkManagedLogGroup": true
}
}
22 changes: 22 additions & 0 deletions deploy/lib/data-proxy-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as cdk from "aws-cdk-lib";
import { aws_ec2 as ec2 } from "aws-cdk-lib";
import { Construct } from "constructs";
import { VercelApiProxy } from "./vercel-api-proxy";

interface DataProxyStackProps extends cdk.StackProps {
vpcId: string;
proxyDomain: string;
}

export class DataProxyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: DataProxyStackProps) {
super(scope, id, props);

const vpc = ec2.Vpc.fromLookup(this, "vpc", { vpcId: props.vpcId });

new VercelApiProxy(this, "vercel-api-proxy", {
vpc,
proxyDomain: props.proxyDomain,
});
}
}
100 changes: 100 additions & 0 deletions deploy/lib/vercel-api-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
aws_ec2 as ec2,
aws_iam as iam,
aws_route53 as route53,
} from "aws-cdk-lib";
import { Construct } from "constructs";

interface VercelApiProxyProps {
vpc: ec2.IVpc;
proxyDomain: string;
}

export class VercelApiProxy extends Construct {
/**
* To work around Vercel's firewall, we must proxy all requests for the Proxy API through
* a Squid proxy. This will allow us to have a stable IP address for the Proxy API which
* we can add to the Vercel firewall's bypass list. This allows us to retain ephemeral IP
* addresses for the Proxy API and to avoid using other techniques like passing data
* through a NAT Gateway which would have considerable cost implications.
*/
constructor(scope: Construct, id: string, props: VercelApiProxyProps) {
super(scope, id);

const proxyPort = 3128;

// Create security group for the proxy
const proxySg = new ec2.SecurityGroup(this, "proxy-sg", {
vpc: props.vpc,
description: "Allow inbound from ECS for Squid proxy",
allowAllOutbound: true,
});

// Allow ECS (internal) traffic on port 3128
proxySg.addIngressRule(
ec2.Peer.ipv4(props.vpc.vpcCidrBlock),
ec2.Port.tcp(proxyPort),
"Allow ECS to connect to Squid"
);

// Squid install and minimal config
const userData = ec2.UserData.forLinux();
userData.addCommands(
"yum update -y",
"yum install -y squid",

// Write squid.conf using heredoc
"cat <<'EOF' > /etc/squid/squid.conf",
`http_port ${proxyPort}`,
"acl all src 0.0.0.0/0",
"http_access allow all",
"EOF",

// Enable and start Squid
"systemctl enable squid",
"systemctl restart squid"
);

// Enable SSM access for the EC2 instance
const ssmRole = new iam.Role(this, "ec2-ssm-role", {
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"AmazonSSMManagedInstanceCore"
),
],
});

// Launch EC2 instance
const instance = new ec2.Instance(this, "squid-proxy", {
vpc: props.vpc,
role: ssmRole,
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.MICRO
),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
securityGroup: proxySg,
userData,
disableApiTermination: true,
});

// Allocate and associate Elastic IP
const eip = new ec2.CfnEIP(this, "proxy-eip", {});
new ec2.CfnEIPAssociation(this, "proxy-eip-assoc", {
allocationId: eip.attrAllocationId,
instanceId: instance.instanceId,
});

// Route 53 Private Hosted Zone
const zone = new route53.PrivateHostedZone(this, "proxy-zone", {
vpc: props.vpc,
zoneName: props.proxyDomain,
});
new route53.ARecord(this, "proxy-a-record", {
zone,
target: route53.RecordTarget.fromIpAddresses(instance.instancePrivateIp),
});
}
}
Loading