Integrating with AWS Secrets Manager
Objective
In this lab we'll be discussing Kubernetes Secrets, the security precautions to be aware of when using them, and the use of AWS Secrets Manager as an alternative. We'll be going through the following steps during our exercise:
- Review our implementation in the FastAPI application
- Create our secret in AWS Secrets Manager
- Build and deploy the new version of the application to our EKS cluster
Prerequisites
Initial Setup
Navigate to the root directory of the python-fastapi-demo-docker
project where your environment variables are sourced:
cd ~/environment/python-fastapi-demo-docker
When using Kubernetes Secrets there are a number of security considerations to keep in mind to ensure that they are not inadvertently exposed. The following pages are a good starting place for Kubernetes in general and EKS specifically.
1. Reviewing Our Implementation for Fetching Secrets
As mentioned in some of the links above on Secrets best practices both volume mounts and environment variables have potential security pitfalls that are important to be aware of. For this reason, we've gone with the most secure path by cutting out Kubernetes Secrets from the equation. Instead, we're using Boto3 (AWS SDK for Python) and making calls to AWS Secrets Manager directly. The code snippet below from connect.py shows how we're making this work:
# Retrieves secret by name from AWS Secrets Manager
def get_secret(secret_name):
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager'
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
print("The requested secret " + secret_name + " was not found")
elif e.response['Error']['Code'] == 'InvalidRequestException':
print("The request was invalid due to:", e)
elif e.response['Error']['Code'] == 'InvalidParameterException':
print("The request had invalid params:", e)
elif e.response['Error']['Code'] == 'DecryptionFailure':
print("The requested secret can't be decrypted using the provided KMS key:", e)
elif e.response['Error']['Code'] == 'InternalServiceError':
print("An error occurred on service side:", e)
else:
# Secrets Manager decrypts the secret value using the associated KMS CMK
# Depending on whether the secret was a string or binary, only one of these fields will be populated
if 'SecretString' in get_secret_value_response:
return get_secret_value_response['SecretString']
else:
return get_secret_value_response['SecretBinary']
secret_data = get_secret('eksdevworkshop-db-url')
# Parse the json string in secret_data and extract connectionstring
DATABASE_URL = json.loads(secret_data)['connectionstring']
The get_secret()
function accepts the SecretId
of the Secret we're looking to fetch from Secrets Manager. We handle a number of potential error cases and should the retrieval succeed we then look for either the SecretString
or SecretBinary
keys in the response. Whichever one is present is what we return back to the caller. Once the JSON data for the Secret eksdevworkshop-db-url
is stored in secret_data
we then parse this JSON string and get the value of the connectionstring
key.
Ideally, this secret would be cached if it needs to be used more than once however for our use we've opted to keep things simple. For Python, the aws-secretsmanager-caching
package is available to use.
2. Create Our Secret in Secrets Manager
Now that we know how the fetching from Secrets Manager is being done in the code let's go ahead and actually create this Secret using the awscli
.
First, we need to make sure we have our environment variable loaded by running the command:
source .env
Next, let's show the value of the DOCKER_DATABASE_URL
variable:
echo $DOCKER_DATABASE_URL
The output should look like this:
postgresql://bookdbadmin:dbpassword@db:5432/bookstore
With this value, we can run the following command to create our secret:
SECRET_ARN=$(aws --region $AWS_REGION secretsmanager create-secret --name eksdevworkshop-db-url --secret-string '{"connectionstring":"postgresql://bookdbadmin:dbpassword@db:5432/bookstore"}' --query 'ARN')
With our secret now created we'll need to create an IAM policy document which provides permissions to access this Secret. We can do this with the following command:
cat << EOF > fastapi-policy.json
{
"Version": "2012-10-17",
"Statement": [ {
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
"Resource": [${SECRET_ARN}]
} ]
}
EOF
This creates the file fastapi-policy.json
in the root of our project folder. Let's confirm that this file looks as we expect:
cat fastapi-policy.json
The output should should look as follows:
{
"Version": "2012-10-17",
"Statement": [ {
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
"Resource": ["arn:aws:secretsmanager:region:012345678901:secret:eksdevworkshop-db-url-xxxxxx"]
} ]
}
We can now create our IAM policy using this JSON document with this command:
POLICY_ARN=$(aws --region $AWS_REGION --query Policy.Arn --output text iam create-policy --policy-name fastapi-secrets-access --policy-document file://fastapi-policy.json)
Lastly, we'll need to create our IAM Role for the FastAPI application using this policy and create our serviceaccount
for IRSA to provide credentials for this role to our application.
- Fargate
- Managed Node Groups
eksctl create iamserviceaccount --name fastapi-deployment-sa --region $AWS_REGION --cluster fargate-quickstart --attach-policy-arn $POLICY_ARN --namespace my-cool-app --approve --override-existing-serviceaccounts
eksctl create iamserviceaccount --name fastapi-deployment-sa --region $AWS_REGION --cluster managednode-quickstart --attach-policy-arn $POLICY_ARN --namespace my-cool-app --approve --override-existing-serviceaccounts
After a minute or two of CloudFormation templates running we should see success messages showing us that our IAM Role and serviceaccount
have been created.
3. Build and Deploy the New Version of the Application
We're in the home stretch now and all that's left is to build and deploy our application. To do this, let's first switch to the Git branch aws-secrets-manager-lab
which has our updated application code and deployment manifest.
You may receive an error like error: Your local changes to the following files would be overwritten by checkout
when running the below command if there are changes made to any of the source files. To fix this, first run the command git stash
and then run the below command. After the checkout is finished, run the command git stash pop
to reapply the changes.
git checkout aws-secrets-manager-lab
Next we'll update our IMAGE_VERSION
environment variable to use a new version:
It's important to use a new image version as cached images on worker nodes can result in unexpected behaviors.
export IMAGE_VERSION=2.0
Let's get our credentials for ECR and login with Docker so we can push our image to our repository using this command:
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
Now, we can run the following commands to create our multi-architecture build environment, build, tag, and push our image to ECR:
docker buildx create --name webBuilder
docker buildx use webBuilder
docker buildx inspect --bootstrap
docker buildx build --platform linux/amd64,linux/arm64 -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/fastapi-microservices:$IMAGE_VERSION . --push
With our new image successfully uploaded to ECR we can now update our deployment file eks/deploy-app-with-secrets-manager.yaml
to use our new image. First, retrieve your Amazon ECR repository URI using the following command:
echo ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/fastapi-microservices:${IMAGE_VERSION}
The output should look as follows:
012345678901.dkr.ecr.us-east-1.amazonaws.com/fastapi-microservices:2.0
We can take this URI and update our deployment file eks/deploy-app-with-secrets-manager.yaml
on line 32 to now be as follows:
image: 012345678901.dkr.ecr.us-east-1.amazonaws.com/fastapi-microservices:2.0
Lastly let's deploy this new version to our EKS cluster with the following command:
kubectl apply -f eks/deploy-app-with-secrets-manager.yaml
The output should look as follows:
service/fastapi-service unchanged
deployment.apps/fastapi-deployment configured
ingress.networking.k8s.io/fastapi-ingress unchanged
We can watch our deployment's progress by watching the pods in the my-cool-app
namespace and waiting until our new pod reaches the Running
state:
kubectl get pods -n my-cool-app --watch
Let's take a look at our pod logs to make sure everything is working as expected:
kubectl logs deploy/fastapi-deployment -n my-cool-app
We'll see something like the following once it's ready:
NAME READY STATUS RESTARTS AGE
fastapi-deployment-bbfd7b7b4-d6cgh 1/1 Running 0 1m
fastapi-postgres-0 1/1 Running 0 18h
We should see the following in the output showing that our exercise has been successful:
INFO:server.app.connect:Trying to connect to db:5432 as bookdbadmin...
INFO:server.app.connect:Connection successful!
INFO: Started server process [6]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Database URL: postgresql://bookdbadmin:dbpassword@db:5432/bookstore
INFO: 192.168.33.158:42464 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.25.22:30050 - "GET / HTTP/1.1" 200 OK
4. Cleanup Resources
To delete the resources we created during this lab run the following commands:
- Fargate
- Managed Node Groups
kubectl delete -f eks/deploy-app-with-secrets-manager.yaml
aws secretsmanager delete-secret --region $AWS_REGION --force-delete-without-recovery --secret-id eksdevworkshop-db-url
eksctl delete iamserviceaccount --name fastapi-deployment-sa --region $AWS_REGION --cluster fargate-quickstart --namespace my-cool-app
aws iam delete-policy --region $AWS_REGION --policy-arn $POLICY_ARN
kubectl delete -f eks/deploy-app-with-secrets-manager.yaml
aws secretsmanager delete-secret --region $AWS_REGION --force-delete-without-recovery --secret-id eksdevworkshop-db-url
eksctl delete iamserviceaccount --name fastapi-deployment-sa --region $AWS_REGION --cluster managednode-quickstart --namespace my-cool-app
aws iam delete-policy --region $AWS_REGION --policy-arn $POLICY_ARN