Skip to content

Commit

Permalink
add showmigrations lambda (#736)
Browse files Browse the repository at this point in the history
* add showmigrations lambda

* debug lambda

* update show migrations V2

* import aws lambdas utils

* add debug message

* Add pulumi for lambda & lambda call from github actions

* fix github actions

* fix github actions work dir

* pass docker image tag

* pass docker image tag

* update dependency

* update dependency

* update envs for lambda call

* update lambda call

* setup secret key

* update stack name

* remove unused file
  • Loading branch information
larisa17 authored Nov 25, 2024
1 parent dcc8264 commit e1e06b2
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 2 deletions.
66 changes: 65 additions & 1 deletion .github/workflows/build_and_deploy_generic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,70 @@ jobs:
dockerfile_name: ./verifier/Dockerfile
build_dir: ./verifier/

check_migrations:
needs: [docker-lambda]
name: Run showmigrations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ inputs.refspec }}
fetch-depth: 0
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
- name: Configure 1Password Service Account
uses: 1password/load-secrets-action/configure@v1
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Load secret
id: op-load-secret
uses: 1password/load-secrets-action@v1
with:
export-env: true
env:
AWS_ACCESS_KEY_ID: op://DevOps/passport-scorer-${{ inputs.environment }}-secrets/ci/AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: op://DevOps/passport-scorer-${{ inputs.environment }}-secrets/ci/AWS_SECRET_ACCESS_KEY
PULUMI_ACCESS_TOKEN: op://DevOps/passport-scorer-${{ inputs.environment }}-secrets/ci/PULUMI_ACCESS_TOKEN
- name: Prepare to Deploy to AWS
uses: passportxyz/gh-workflows/.github/actions/prepare_deploy_to_aws@v1

- name: Setup pulumi for showmigrations
uses: pulumi/actions@v4
id: pulumi-up
with:
stack-name: passportxyz/passport-scorer-ops/${{ inputs.environment }}
command: up
work-dir: ./infra/ops
env:
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
PULUMI_ACCESS_TOKEN: ${{ env.PULUMI_ACCESS_TOKEN }}
AWS_REGION: us-west-2
DOCKER_IMAGE_TAG: ${{ inputs.docker_tag }}
- name: Get Lambda Function URL
run: |
echo "Extracting Lambda Function URL..."
URL=$(pulumi stack output lambdaUrl --stack passportxyz/passport-scorer-ops/${{ inputs.environment }})
echo "LAMBDA_FUNCTION_URL=$URL" >> $GITHUB_ENV
id: get-lambda-url
- uses: actions/setup-python@v4
with:
python-version: "3.12"

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install requests requests-aws4auth
- name: Run showmigrations (call lambda function)
env:
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-west-2
LAMBDA_FUNCTION_URL: ${{ env.LAMBDA_FUNCTION_URL }}
run: |
python infra/scripts/ops/lambda_call.py
deploy_preview:
name: Preview - Deploying AWS Infra
runs-on: ubuntu-latest
Expand Down Expand Up @@ -161,7 +225,7 @@ jobs:

deploy_confirm:
name: Review Approval Pending
needs: [deploy_preview]
needs: [check_migrations, deploy_preview]
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
Expand Down
23 changes: 22 additions & 1 deletion api/aws_lambdas/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

def load_secrets():
ssm_srn = os.environ["SCORER_SERVER_SSM_ARN"]

print("Loading secrets from SSM: ", ssm_srn)
# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(service_name="secretsmanager")
Expand All @@ -57,6 +57,27 @@ def load_secrets():
if "SCORER_SERVER_SSM_ARN" in os.environ:
load_secrets()

if "CORE_SECRET_ARN" in os.environ:
core_secret_arn = os.environ["CORE_SECRET_ARN"]
print("Loading secrets from SSM: ", core_secret_arn)
# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(service_name="secretsmanager")

try:
get_secret_value_response = client.get_secret_value(SecretId=core_secret_arn)
except ClientError as e:
print(f"Error occurred while loading secret value: {e}")
print_exc()
raise e
secrets = json.loads(get_secret_value_response["SecretString"])
db_user = secrets["username"]
db_password = secrets["password"]
db_host = secrets["host"]
db_name = secrets["dbname"]
db_url = f"psql://{db_user}:{db_password}@{db_host}/{db_name}"
os.environ["DATABASE_URL"] = db_url

###########################################################
# END: Loading secrets from secrets manager
###########################################################
Expand Down
22 changes: 22 additions & 0 deletions api/v2/aws_lambdas/showmigrations_GET.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from io import StringIO

from django.core.management import call_command

# pylint: disable=unused-import
import aws_lambdas.utils


# pylint: disable=unused-argument
def handler(event, context):
"""
Run show migrations command and return the output.
"""

try:
print("Running showmigrations command")
output = StringIO()
call_command("showmigrations", stdout=output)
print("Done running showmigrations command")
return {"statusCode": 200, "body": output.getvalue()}
except Exception as e:
return {"statusCode": 500, "body": str(e)}
7 changes: 7 additions & 0 deletions infra/ops/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: passport-scorer-ops
runtime: nodejs
description: A project to manage ops tools
config:
pulumi:tags:
value:
pulumi:template: typescript
14 changes: 14 additions & 0 deletions infra/ops/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as pulumi from "@pulumi/pulumi";

export const stack = pulumi.getStack(); // values : review, staging & production

export const coreInfraOutputs = new pulumi.StackReference(
`passportxyz/core-infra/${stack}`
);

export const coreRdsSecretArn = coreInfraOutputs.getOutput("coreRdsSecretArn");
export const coreVpcId = coreInfraOutputs.getOutput("vpcId");
export const corePrivateSubnetIds =
coreInfraOutputs.getOutput("privateSubnetIds");

export const rdsSecretArn = coreInfraOutputs.getOutput("rdsSecretArn");
29 changes: 29 additions & 0 deletions infra/ops/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import { createLambdaFunction } from "./lambda";
import { stack, coreVpcId, corePrivateSubnetIds, rdsSecretArn } from "./config";

const dockerImageTag = process.env.DOCKER_IMAGE_TAG;
const awsAccNo = aws.getCallerIdentity().then((caller) => caller.accountId);

const dockerCmd = ["v2.aws_lambdas.showmigrations_GET.handler"];

const { lambdaFunction, lambdaFunctionUrl } = pulumi
.all([coreVpcId, corePrivateSubnetIds, awsAccNo, rdsSecretArn])
.apply(([vpcId, subnetIds, _awsAccNo, _rdsSecretArn]) => {
const dockerImageUri = `${_awsAccNo}.dkr.ecr.us-west-2.amazonaws.com/submit-passport-lambdas:${dockerImageTag}`;
return createLambdaFunction(
"showmigrations",
"Run showmigrations cmd",
dockerImageUri,
dockerCmd,
vpcId,
subnetIds,
[_rdsSecretArn],
{
CORE_SECRET_ARN: _rdsSecretArn,
SECRET_KEY: "1234",
}
);
});
export const lambdaUrl = lambdaFunctionUrl.functionUrl.apply((url) => url);
177 changes: 177 additions & 0 deletions infra/ops/lambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
import { defaultTags } from "./tags";
import { coreRdsSecretArn } from "./config";
//////////////////////////////////////////////////////////////
// Create a Lambda function
//////////////////////////////////////////////////////////////

export function createLambdaFunction(
name: string,
lambdaDescription: string,
dockerImageUri: string,
dockerCmd: string[],
vpcId: string,
vpcSubnetIds: string[],
secretManagerArns: string[],
environmentVariables: Record<string, string>
) {
// manage lambda role

const lambdaRole = new aws.iam.Role(`${name}-role`, {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Action: "sts:AssumeRole",
Principal: {
Service: "lambda.amazonaws.com",
},
Effect: "Allow",
Sid: `${name}LambdaAssumeRole`,
},
],
}),
tags: {
...defaultTags,
Name: `${name}-role`,
},
});

// Manage log group permissions
const logPolicy = new aws.iam.Policy(`${name}-log-policy`, {
name: `${name}-log-policy`,
policy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Action: ["logs:*"],
Effect: "Allow",
Resource: `arn:aws:logs:*:*:*`,
},
],
}),
});
new aws.iam.RolePolicyAttachment(`${name}-log-policy-attachment`, {
policyArn: logPolicy.arn,
role: lambdaRole.name,
});

// TODO: function accepts a list of additional policies to attach to the role .
if (vpcId) {
// add VPC required permissions
const vpcPolicy = new aws.iam.Policy(`${name}-vpc-policy`, {
name: `${name}-vpc-policy`,
policy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Action: [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
],
Effect: "Allow",
Resource: "*",
},
],
}),
});

new aws.iam.RolePolicyAttachment(`${name}-vpc-policy-attachment`, {
policyArn: vpcPolicy.arn,
role: lambdaRole.name,
});
}

// Manage secrets manager
if (secretManagerArns.length > 0) {
const secretPolicy = new aws.iam.Policy(`${name}-secret-policy`, {
name: `${name}-secret-policy`,
policy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Action: ["secretsmanager:GetSecretValue"],
Effect: "Allow",
Resource: secretManagerArns,
},
],
}),
});
new aws.iam.RolePolicyAttachment(`${name}-secret-policy-attachment`, {
policyArn: secretPolicy.arn,
role: lambdaRole.name,
});
}

// This should be created & parsed only if VPC id is provided
const lambdaSecurityGroup = new aws.ec2.SecurityGroup(`${name}-sg`, {
vpcId: vpcId,
ingress: [
{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"], //TODO: this should be restricted
},
],
egress: [
{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
},
],
tags: {
...defaultTags,
Name: `${name}-sg`,
},
});

const lambdaLogGroup = new aws.cloudwatch.LogGroup(`${name}-log-group`, {
name: `/aws/lambda/${name}`,
retentionInDays: 14,
tags: {
...defaultTags,
Name: `${name}-log-group`,
},
});

const lambdaFunction = new aws.lambda.Function(`${name}-function`, {
name: name,
description: lambdaDescription,
role: lambdaRole.arn,
packageType: "Image",
imageUri: dockerImageUri,
imageConfig: {
commands: dockerCmd,
},
memorySize: 128,
timeout: 120,
vpcConfig: {
securityGroupIds: [lambdaSecurityGroup.id],
subnetIds: vpcSubnetIds,
},
loggingConfig: {
logFormat: "Text", // select between Text and structured JSON format for your function's logs.
logGroup: lambdaLogGroup.name,
// systemLogLevel : "DEBUG" // for JSON structured logs, choose the detail level of the Lambda platform event logs sent to CloudWatch, such as ERROR, DEBUG, or INFO.
},
environment: {
variables: environmentVariables,
},
});

//TODO: make the creation of URL conditional
const lambdaFunctionUrl = new aws.lambda.FunctionUrl(`${name}-url`, {
functionName: lambdaFunction.name,
authorizationType: "AWS_IAM", // Set to "NONE" to bypass IAM authentication and create a public endpoint.
});

return {
lambdaFunction,
lambdaFunctionUrl,
};
}
10 changes: 10 additions & 0 deletions infra/ops/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { stack } from "./config";

export const defaultTags = {
Application: "ops",
Repo: "https://github.com/passportxyz/core-infra",
PulumiStack: stack,
Environment: stack,
ManagedBy: "pulumi",
Name: "missing",
};
Loading

0 comments on commit e1e06b2

Please sign in to comment.