Today was a lesson in reading what a system is actually telling you. Git stopped and said it could not decide, which is the most honest thing a tool can do. Lambda said 200, which sounds like success and is not quite a claim about success at all.
One Git task, one AWS task. Resolve a merge conflict after a rejected push, then deploy a Python Lambda function under a purpose-built IAM role. The tasks come from the KodeKloud Engineer platform.
The rejected push is the warning, not the problem
The setup was ordinary. Log in as a different user this time, finish a half-done edit in a shared blog repository, commit it, and push.
ssh max@ststor01
cd /home/max/story-blog
vi story-index.txt
git status
git add .
git commit -m "Add the fourth story to the index"
git push
# ! [rejected] master -> master (fetch first)
Another developer had already pushed to master. My push was not a fast-forward, so Git refused it.
It is worth sitting with that refusal for a second, because the instinct is to treat it as an obstacle. It is not. Git is telling you that accepting this push would drop commits that already exist on the remote. Forcing here would delete a colleague's work. Fetching and merging first is the only correct response.
git pull
# Auto-merging story-index.txt
# CONFLICT (content): Merge conflict in story-index.txt
# Automatic merge failed; fix conflicts and then commit the result.
We had both edited the same line. Git merges changes to different parts of the same file without any drama. A conflict means two people had an opinion about the same region, and Git will not pick one.
<<<<<<< HEAD
4. The Lion and the Mooshika : Ankit
=======
4. Mowgli and the Wolves : Sarah
>>>>>>> 8f2c1a9b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f89
Three marker lines, not two. <<<<<<< HEAD opens your side, ======= divides, >>>>>>> closes theirs. All three have to be deleted. A stray ======= left behind is one of the most common ways a conflict resolution gets committed broken, because the file still parses fine to a human skimming it.
Resolving means writing the text you actually want. It is not a choice between two buttons. Sometimes it is your line, sometimes theirs, and often it is both kept or a rewrite of the two.
vi story-index.txt # edit and remove all three markers
git add story-index.txt
git status # the file leaves "Unmerged paths"
git commit -m "Resolve merge conflict in story-index.txt"
git push
git add on a conflicted file is how you tell Git you are done with it. That is the only signal it has, which is why git status during a conflict is worth reading properly: "Unmerged paths" is your remaining work, and the list shrinks as you stage.
Two things worth having ready before you need them. git merge --abort puts everything back to the pre-merge state, and it is the right move the moment a conflict looks bigger than you expected. Today was a lesson in reading what a system is actually telling you. Git stopped and said it could not decide, which is the most honest thing a tool can do. Lambda said 200, which sounds like success and is not quite a claim about success at all.
One Git task, one AWS task. Resolve a merge conflict after a rejected push, then deploy a Python Lambda function under a purpose-built IAM role. The tasks come from the KodeKloud Engineer platform.
The rejected push is the warning, not the problem
The setup was ordinary. Log in as a different user this time, finish a half-done edit in a shared blog repository, commit it, push.
ssh max@ststor01
cd /home/max/story-blog
vi story-index.txt
git status
git add .
git commit -m "Add the fourth story to the index"
git push
# ! [rejected] master -> master (fetch first)
Another developer had already pushed to master. My push was not a fast-forward, so Git refused it.
It is worth sitting with that refusal for a second, because the instinct is to treat it as an obstacle. It is not. Git is telling you that accepting this push would drop commits that already exist on the remote. Forcing here would delete a colleague's work. Fetching and merging first is the only correct response.
git pull
# Auto-merging story-index.txt
# CONFLICT (content): Merge conflict in story-index.txt
# Automatic merge failed; fix conflicts and then commit the result.
We had both edited the same line. Git merges changes to different parts of the same file without any drama. A conflict means two people had an opinion about the same region, and Git will not pick one.
<<<<<<< HEAD
4. The Lion and the Mooshika : Ankit
=======
4. Mowgli and the Wolves : Sarah
>>>>>>> 8f2c1a9b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f89
Three marker lines, not two. <<<<<<< HEAD opens your side, ======= divides, >>>>>>> closes theirs. All three have to be deleted. A stray ======= left behind is one of the most common ways a conflict resolution gets committed broken, because the file still parses fine to a human skimming it.
Resolving means writing the text you actually want. It is not a choice between two buttons. Sometimes it is your line, sometimes theirs, and often it is both kept or a rewrite of the two.
vi story-index.txt # edit, and remove all three markers
git add story-index.txt
git status # the file leaves "Unmerged paths"
git commit -m "Resolve merge conflict in story-index.txt"
git push
git add on a conflicted file is how you tell Git you are done with it. That is the only signal it has, which is why git status during a conflict is worth reading properly: "Unmerged paths" is your remaining work, and the list shrinks as you stage.
Two things worth having ready before you need them. git merge --abort puts everything back to the pre-merge state, and it is the right move the moment a conflict looks bigger than you expected. And git config --global rerere.enabled true makes Git record how you resolved a conflict and replay that resolution when the same one reappears, which it will on any long-lived branch.
The cheapest fix, of course, sits upstream of all of it. Pull before you start work, not after you finish it.
Lambda: four parts, and two status codes that mean different things
The AWS task was a Python function returning a fixed greeting, running under a role called lambda_execution_role. The task said to use the console; the CLI produces identical resources and is repeatable, so I did it there.
A Lambda deployment from nothing is always four parts, in order, and missing any one of them fails: a role Lambda is allowed to assume, a permissions policy on that role, zipped code, and the function wired to both.
The first two get conflated constantly. The trust policy answers who is allowed to become this role, and the answer has to be the Lambda service principal:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
It grants no permissions at all. Those come separately, and AWSLambdaBasicExecutionRole is the AWS-managed minimum: create a log group, create a log stream, put log events, nothing else.
aws iam attach-role-policy --role-name lambda_execution_role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Note the service-role/ in that ARN. Dropping it produces a "policy does not exist" error that sends you looking in entirely the wrong place.
Then the packaging details, both of which fail quietly rather than loudly:
(cd /root/nautilus-lambda && zip -q /root/nautilus-lambda.zip lambda_function.py)
aws lambda create-function --region us-east-1 \
--function-name nautilus-lambda \
--runtime python3.12 \
--role "$ROLE_ARN" \
--handler lambda_function.lambda_handler \
--zip-file fileb:///root/nautilus-lambda.zip
The .py has to be at the root of the archive. Zip the parent directory and you get nautilus-lambda/lambda_function.py inside, which Lambda cannot import. The handler string is module.function with no extension, so lambda_function.lambda_handler means call lambda_handler inside lambda_function.py; get it wrong and the deploy succeeds and the invoke fails. And it is fileb://, not file://, because the zip is binary and reading it as text corrupts it.
There is also a race worth knowing. Call create-function immediately after create-role and you get "The role defined for the function cannot be assumed by Lambda". The role exists, IAM is eventually consistent, and Lambda cannot see it yet. It clears in five to fifteen seconds. A retry loop beats a guessed sleep, and the same race turns up any time you create an IAM role and immediately hand it to ECS, CodeBuild or EC2.
Now the part I actually want to flag:
aws lambda invoke --region us-east-1 --function-name nautilus-lambda \
/root/lambda-out.json --query 'StatusCode' --output text
cat /root/lambda-out.json
200
{"statusCode": 200, "body": "Welcome to KKE AWS Labs!"}
Two 200s, and they are not the same claim.
The outer one is the Lambda service saying it received the request and ran the function. AWS states it plainly in the Invoke API reference: the status code in the API response does not reflect function errors, and error codes are reserved for problems that stop the function executing at all, such as permissions or configuration. A function that throws an unhandled exception still returns 200 there.
The inner statusCode is your own code's return value. That is the one that proves the function did what you wrote.
If the function had failed, the response would also carry a FunctionError field and the payload would hold a stack trace. Which means checking only the outer status code is a false positive waiting to happen, and it is exactly the check a hurried health script tends to make.
One small trap alongside it: invoke writes the payload to the file argument, it does not print to stdout. Leave the path off and you get a usage error rather than your output.
Read what it said, not what you expected
A merge conflict and a 200 look like opposites. One is a stop, the other is the number you were hoping for. They are the same lesson from two directions.
Git refused to guess and told you exactly which lines it could not decide about, which is more information than a silent success would have given you. Lambda answered a narrower question than the one you were asking, accurately, and it is on you to know which question that was.
So here is the Day 33 question. The green check in your pipeline: do you know precisely which claim it is making, and which one it is not?
Day 33 down. Sixty-seven to go.
Top comments (0)