Skip to content
Open
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
40 changes: 26 additions & 14 deletions .github/workflows/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand All @@ -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}
Expand All @@ -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 }}
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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 }}
Expand Down
228 changes: 228 additions & 0 deletions POST_DEPLOYMENT_SETUP.md
Original file line number Diff line number Diff line change
@@ -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"
```
Comment on lines +138 to +150
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace hardcoded API endpoints with placeholders.

Lines 144, 147, and 218 contain hardcoded API Gateway endpoints (j31g91e2fa.execute-api.us-east-1.amazonaws.com). Users will need to replace these with their actual deployed endpoints.

🔎 Proposed fix
 ```bash
 # 1. Health check (should work)
-curl https://j31g91e2fa.execute-api.us-east-1.amazonaws.com/Prod/health-check
+curl https://<YOUR_API_ID>.execute-api.<YOUR_REGION>.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/" \
+curl -X GET "https://<YOUR_API_ID>.execute-api.<YOUR_REGION>.amazonaws.com/Prod/feature-flags/" \
   -H "Cookie: rds-session-staging=YOUR_JWT_TOKEN" \
   -H "Origin: https://test.realdevsquad.com"

Add a note explaining how to find the API Gateway endpoint:

```markdown
**Note:** Replace `<YOUR_API_ID>` and `<YOUR_REGION>` with your actual API Gateway ID and region. You can find this in:
- AWS Console: API Gateway → Your API → Stages → Prod
- Or run: `aws cloudformation describe-stacks --stack-name <YOUR_STACK_NAME> --query 'Stacks[0].Outputs'`
🤖 Prompt for AI Agents
In POST_DEPLOYMENT_SETUP.md around lines 138 to 150 (and also update the other
occurrence at line 218), replace the hardcoded API Gateway URLs
(j31g91e2fa.execute-api.us-east-1.amazonaws.com) with placeholders like
<YOUR_API_ID>.execute-api.<YOUR_REGION>.amazonaws.com and update the two curl
examples accordingly; add a short note telling users how to find their API ID
and region (AWS Console path and an aws cloudformation describe-stacks command)
so they can substitute the placeholders with their actual values.


---

## 📋 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
Comment on lines +152 to +228
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Excellent troubleshooting and reference sections!

The documentation concludes with valuable resources:

  • Actionable checklist for post-deployment verification
  • Comprehensive troubleshooting guide covering common error scenarios
  • Quick setup workflow for rapid deployment
  • Important notes about billing mode and configuration

Minor formatting: Line 184 has a trailing space (detected by markdownlint).

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

184-184: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)

🤖 Prompt for AI Agents
In POST_DEPLOYMENT_SETUP.md around lines 152 to 228, there is a minor formatting
issue: line 184 contains a trailing space flagged by markdownlint; remove the
trailing whitespace on line 184 (and re-save the file) so the line ends cleanly
with no extra spaces, then run your linter/markdownlint to confirm the fix.

26 changes: 24 additions & 2 deletions layer/jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}

Expand All @@ -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) {
Expand Down
Loading