diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index bd7eb37..bbe9455 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -14,13 +14,13 @@ jobs: name: Prepare runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@v3.x + uses: rlespinasse/github-slug-action@v5 - name: Prepare Outputs id: prepare-step run: | - echo "::set-output name=branch_name::${GITHUB_REF_SLUG}"; + echo "branch_name=${GITHUB_REF_SLUG}" >> $GITHUB_OUTPUT outputs: branch_name: ${{ steps.prepare-step.outputs.branch_name }} @@ -29,13 +29,19 @@ jobs: runs-on: ubuntu-latest environment: ${{ needs.prepare.outputs.branch_name }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: '1.25.1' - - uses: actions/setup-python@v2 - - uses: aws-actions/setup-sam@v1 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + - name: Setup SAM CLI + uses: aws-actions/setup-sam@v2 + with: + use-installer: true - name: Build resources run: sam build --template ${SAM_TEMPLATE} @@ -46,7 +52,7 @@ jobs: PIPELINE_USER_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} PIPELINE_EXECUTION_ROLE: ${{ secrets.PIPELINE_EXECUTION_ROLE }} REGION: ${{ secrets.REGION }} - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ env.PIPELINE_USER_ACCESS_KEY_ID }} aws-secret-access-key: ${{ env.PIPELINE_USER_SECRET_ACCESS_KEY }} @@ -65,7 +71,7 @@ jobs: --s3-bucket ${ARTIFACTS_BUCKET} \ --region ${REGION} \ --output-template-file packaged.yaml - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: packaged.yaml path: packaged.yaml @@ -75,10 +81,16 @@ jobs: runs-on: ubuntu-latest environment: ${{ needs.prepare.outputs.branch_name }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v2 - - uses: aws-actions/setup-sam@v1 - - uses: actions/download-artifact@v4 + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + - name: Setup SAM CLI + uses: aws-actions/setup-sam@v2 + with: + use-installer: true + - uses: actions/download-artifact@v6 with: name: packaged.yaml @@ -88,7 +100,7 @@ jobs: PIPELINE_USER_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} PIPELINE_EXECUTION_ROLE: ${{ secrets.PIPELINE_EXECUTION_ROLE }} REGION: ${{ secrets.REGION }} - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ env.PIPELINE_USER_ACCESS_KEY_ID }} aws-secret-access-key: ${{ env.PIPELINE_USER_SECRET_ACCESS_KEY }} diff --git a/POST_DEPLOYMENT_SETUP.md b/POST_DEPLOYMENT_SETUP.md new file mode 100644 index 0000000..45f4cab --- /dev/null +++ b/POST_DEPLOYMENT_SETUP.md @@ -0,0 +1,228 @@ +# Post-Deployment Setup Guide + +After deploying with `sam deploy`, you need to set up the following resources: + +## ๐Ÿ”ด Critical: DynamoDB Tables + +The error you're seeing is because **DynamoDB tables don't exist**. You need to create 3 tables: + +### Quick Setup (Using Script) + +```bash +# Make script executable +chmod +x setup-dynamodb-tables.sh + +# Run the script (defaults to us-east-1) +./setup-dynamodb-tables.sh + +# OR specify a region +./setup-dynamodb-tables.sh us-east-1 +``` + +### Manual Setup + +#### 1. Create `featureFlag` Table + +```bash +aws dynamodb create-table \ + --table-name featureFlag \ + --attribute-definitions AttributeName=id,AttributeType=S \ + --key-schema AttributeName=id,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ + --region us-east-1 +``` + +#### 2. Create `featureFlagUserMapping` Table + +```bash +aws dynamodb create-table \ + --table-name featureFlagUserMapping \ + --attribute-definitions \ + AttributeName=userId,AttributeType=S \ + AttributeName=flagId,AttributeType=S \ + --key-schema \ + AttributeName=userId,KeyType=HASH \ + AttributeName=flagId,KeyType=RANGE \ + --billing-mode PAY_PER_REQUEST \ + --region us-east-1 +``` + +#### 3. Create `requestLimit` Table (This is the missing one causing your error!) + +```bash +aws dynamodb create-table \ + --table-name requestLimit \ + --attribute-definitions AttributeName=limitType,AttributeType=S \ + --key-schema AttributeName=limitType,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ + --region us-east-1 +``` + +#### 4. Initialize `requestLimit` Table + +After creating the table, you need to add an initial value: + +```bash +aws dynamodb put-item \ + --table-name requestLimit \ + --item '{ + "limitType": {"S": "pendingLimit"}, + "limitValue": {"N": "1000"} + }' \ + --region us-east-1 +``` + +--- + +## โœ… Verify Tables Exist + +```bash +# List all tables +aws dynamodb list-tables --region us-east-1 + +# Check specific table +aws dynamodb describe-table --table-name requestLimit --region us-east-1 +``` + +You should see: +- `featureFlag` +- `featureFlagUserMapping` +- `requestLimit` + +--- + +## ๐Ÿ”‘ SSM Parameter for JWT Public Key + +Make sure the JWT public key exists in AWS Systems Manager Parameter Store: + +### For PRODUCTION: + +```bash +# Check if parameter exists +aws ssm get-parameter \ + --name PROD_RDS_BACKEND_PUBLIC_KEY \ + --with-decryption \ + --region us-east-1 + +# If it doesn't exist, create it: +aws ssm put-parameter \ + --name PROD_RDS_BACKEND_PUBLIC_KEY \ + --value "-----BEGIN PUBLIC KEY----- +YOUR_PUBLIC_KEY_HERE +-----END PUBLIC KEY-----" \ + --type SecureString \ + --region us-east-1 +``` + +### For DEVELOPMENT: + +```bash +# Check if parameter exists +aws ssm get-parameter \ + --name STAGING_RDS_BACKEND_PUBLIC_KEY \ + --with-decryption \ + --region us-east-1 + +# If it doesn't exist, create it: +aws ssm put-parameter \ + --name STAGING_RDS_BACKEND_PUBLIC_KEY \ + --value "-----BEGIN PUBLIC KEY----- +YOUR_PUBLIC_KEY_HERE +-----END PUBLIC KEY-----" \ + --type SecureString \ + --region us-east-1 +``` + +--- + +## ๐Ÿงช Test After Setup + +After creating the tables, test your API: + +```bash +# 1. Health check (should work) +curl https://j31g91e2fa.execute-api.us-east-1.amazonaws.com/Prod/health-check + +# 2. Get feature flags (should work now) +curl -X GET "https://j31g91e2fa.execute-api.us-east-1.amazonaws.com/Prod/feature-flags/" \ + -H "Cookie: rds-session-staging=YOUR_JWT_TOKEN" \ + -H "Origin: https://test.realdevsquad.com" +``` + +--- + +## ๐Ÿ“‹ Complete Checklist + +- [ ] **DynamoDB Tables Created:** + - [ ] `featureFlag` table exists + - [ ] `featureFlagUserMapping` table exists + - [ ] `requestLimit` table exists + - [ ] `requestLimit` table has initial item with `limitType: "pendingLimit"` and `limitValue: 1000` + +- [ ] **SSM Parameter:** + - [ ] `PROD_RDS_BACKEND_PUBLIC_KEY` exists (for PRODUCTION) + - [ ] OR `STAGING_RDS_BACKEND_PUBLIC_KEY` exists (for DEVELOPMENT) + +- [ ] **Testing:** + - [ ] Health check endpoint works + - [ ] Feature flags endpoints work with JWT token + +--- + +## ๐Ÿ” Troubleshooting + +### Error: "ResourceNotFoundException: Requested resource not found" + +**Cause:** DynamoDB table doesn't exist + +**Solution:** Create the missing table using the commands above + +### Error: "invalid memory address or nil pointer dereference" + +**Cause:** Code is trying to unmarshal a nil response from DynamoDB (table doesn't exist or item doesn't exist) + +**Solution:** +1. Create the `requestLimit` table +2. Initialize it with the default value (see step 4 above) + +### Error: "ParameterNotFound" when calling API + +**Cause:** SSM Parameter for JWT public key doesn't exist + +**Solution:** Create the SSM parameter with the public key (see SSM Parameter section above) + +### Error: "AccessDeniedException" when creating tables + +**Cause:** Your AWS credentials don't have DynamoDB permissions + +**Solution:** Ensure your AWS user/role has: +- `dynamodb:CreateTable` +- `dynamodb:PutItem` +- `dynamodb:DescribeTable` +- `dynamodb:ListTables` + +--- + +## ๐Ÿš€ Quick Setup Command + +Run this to set up everything: + +```bash +# 1. Create all tables +./setup-dynamodb-tables.sh us-east-1 + +# 2. Verify tables +aws dynamodb list-tables --region us-east-1 + +# 3. Test API +curl https://j31g91e2fa.execute-api.us-east-1.amazonaws.com/Prod/health-check +``` + +--- + +## ๐Ÿ“ Notes + +- Tables are created with **PAY_PER_REQUEST** billing mode (no capacity planning needed) +- The `requestLimit` table is initialized with a default value of 1000 +- You can adjust the initial `limitValue` based on your needs +- All tables are created in the same region as your Lambda functions diff --git a/layer/jwt/jwt.go b/layer/jwt/jwt.go index a7ab0d9..a765a29 100644 --- a/layer/jwt/jwt.go +++ b/layer/jwt/jwt.go @@ -85,26 +85,33 @@ func (j *JWTUtils) initialize() error { } } + log.Printf("Attempting to fetch public key from SSM parameter: %s (Environment: %s)", parameterName, envConfig.Environment) publicKeyString, err := getPublicKeyFromParameterStore(parameterName) if err != nil { + log.Printf("Failed to get public key from SSM: %v", err) return err } - + publicKeyString = strings.TrimSpace(publicKeyString) + block, _ := pem.Decode([]byte(publicKeyString)) if block == nil { + log.Printf("Failed to decode PEM block from public key. First 100 chars: %s", publicKeyString[:min(100, len(publicKeyString))]) return errors.New("internal server error") } pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { + log.Printf("Failed to parse PKIX public key: %v", err) return fmt.Errorf("internal server error") } rsaPublicKey, ok := pub.(*rsa.PublicKey) if !ok { + log.Printf("Public key is not an RSA public key") return errors.New("internal server error") } + log.Printf("Successfully initialized JWT utils with public key") j.publicKey = rsaPublicKey return nil } @@ -115,6 +122,7 @@ func getPublicKeyFromParameterStore(parameterName string) (string, error) { cfg, err := config.LoadDefaultConfig(ctx) if err != nil { + log.Printf("Failed to load AWS config: %v", err) return "", err } @@ -126,10 +134,24 @@ func getPublicKeyFromParameterStore(parameterName string) (string, error) { result, err := svc.GetParameter(ctx, input) if err != nil { + log.Printf("Failed to get parameter %s from SSM: %v", parameterName, err) return "", err } - return *result.Parameter.Value, nil + if result.Parameter == nil || result.Parameter.Value == nil { + log.Printf("Parameter %s exists but has no value", parameterName) + return "", errors.New("parameter has no value") + } + + value := strings.TrimSpace(*result.Parameter.Value) + return value, nil +} + +func min(a, b int) int { + if a < b { + return a + } + return b } func (j *JWTUtils) ValidateToken(tokenString string) (jwt.MapClaims, error) { diff --git a/setup-dynamodb-tables.sh b/setup-dynamodb-tables.sh new file mode 100755 index 0000000..6770cfd --- /dev/null +++ b/setup-dynamodb-tables.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Script to create all required DynamoDB tables for feature-flag-backend +# Usage: ./setup-dynamodb-tables.sh [region] + +set -e + +REGION=${1:-us-east-1} + +echo "๐Ÿš€ Setting up DynamoDB tables in region: $REGION" +echo "" + +# Table 1: featureFlag +echo "Creating table: featureFlag" +aws dynamodb create-table \ + --table-name featureFlag \ + --attribute-definitions \ + AttributeName=id,AttributeType=S \ + --key-schema \ + AttributeName=id,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ + --region $REGION \ + --no-cli-pager + +echo "โœ… Created featureFlag table" +echo "" + +# Table 2: featureFlagUserMapping +echo "Creating table: featureFlagUserMapping" +aws dynamodb create-table \ + --table-name featureFlagUserMapping \ + --attribute-definitions \ + AttributeName=userId,AttributeType=S \ + AttributeName=flagId,AttributeType=S \ + --key-schema \ + AttributeName=userId,KeyType=HASH \ + AttributeName=flagId,KeyType=RANGE \ + --billing-mode PAY_PER_REQUEST \ + --region $REGION \ + --no-cli-pager + +echo "โœ… Created featureFlagUserMapping table" +echo "" + +# Table 3: requestLimit +echo "Creating table: requestLimit" +aws dynamodb create-table \ + --table-name requestLimit \ + --attribute-definitions \ + AttributeName=limitType,AttributeType=S \ + --key-schema \ + AttributeName=limitType,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ + --region $REGION \ + --no-cli-pager + +echo "โœ… Created requestLimit table" +echo "" + +# Wait for tables to be active +echo "โณ Waiting for tables to become active..." +aws dynamodb wait table-exists \ + --table-name featureFlag \ + --region $REGION + +aws dynamodb wait table-exists \ + --table-name featureFlagUserMapping \ + --region $REGION + +aws dynamodb wait table-exists \ + --table-name requestLimit \ + --region $REGION + +echo "โœ… All tables are active!" +echo "" + +# Initialize requestLimit table with default value +echo "Initializing requestLimit table with default value..." +aws dynamodb put-item \ + --table-name requestLimit \ + --item '{ + "limitType": {"S": "pendingLimit"}, + "limitValue": {"N": "1000"} + }' \ + --region $REGION \ + --no-cli-pager + +echo "โœ… Initialized requestLimit with default value (1000)" +echo "" + +echo "๐ŸŽ‰ All DynamoDB tables created and initialized successfully!" +echo "" +echo "Tables created:" +echo " - featureFlag" +echo " - featureFlagUserMapping" +echo " - requestLimit (with initial value)" +echo "" +echo "You can now test your API endpoints!" diff --git a/template.yaml b/template.yaml index a984bcb..652fbb6 100644 --- a/template.yaml +++ b/template.yaml @@ -87,6 +87,9 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: requestLimit Events: Preflight: Type: Api @@ -105,7 +108,18 @@ Resources: Architectures: - x86_64 Layers: - - !Ref SharedLayer + - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api @@ -124,7 +138,14 @@ Resources: Architectures: - x86_64 Layers: - - !Ref SharedLayer + - !Ref SharedLayer + Policies: + - Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:PutFunctionConcurrency + Resource: '*' Events: CatchAll: Type: Api @@ -143,7 +164,10 @@ Resources: Architectures: - x86_64 Layers: - - !Ref SharedLayer + - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: requestLimit Events: CatchAll: Type: Api @@ -163,6 +187,17 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api @@ -182,6 +217,17 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api @@ -201,6 +247,17 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api @@ -220,6 +277,17 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api @@ -239,6 +307,17 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api @@ -258,6 +337,17 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api @@ -277,6 +367,17 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api @@ -296,6 +397,17 @@ Resources: - x86_64 Layers: - !Ref SharedLayer + Policies: + - DynamoDBCrudPolicy: + TableName: featureFlag + - DynamoDBCrudPolicy: + TableName: featureFlagUserMapping + - DynamoDBCrudPolicy: + TableName: requestLimit + - SSMParameterReadPolicy: + ParameterName: PROD_RDS_BACKEND_PUBLIC_KEY + - SSMParameterReadPolicy: + ParameterName: STAGING_RDS_BACKEND_PUBLIC_KEY Events: CatchAll: Type: Api