The "Unauthorized" issue
A company I joined recently wants to run a new product. Being the first developer in the new project I had an opportunity to have my hands on pretty basic operational stuff. Saying operational I mean to do the tasks that make me slow a little bit down, for now, to accelerate later. For instance, according to the best practices[4][5][6] I desired to have the "Hello World" web app on production.
You would say, easy-peasy huh?. That is exactly what I thought. Dockerfile, a few YAML files. Done. Next work item.
Well, yeah. It looked like that until I added a reference to a package from a private Azure Artifacts feed. After then I bumped into a message: "401 Unauthorized" every time I tried to containerize an app in the Docker build. It took me several hours, starting from googling through blogs, finding bugs, and finally ending up on the GitHub reading source code of Azure Tasks to figure it out.
Let's take a step back and think about what I tried to achieve.
But, why?
I strongly believe that we should containerize every modern web app. However, let's forget about the runtime for now and focus on the building inside a container. You may ask why you should build inside a container. It's simple. Look.
Containers are isolated from other apps and background processes. Nothing can accidentally break it, nor accidentally make it pass. If a build fails, I bet it is because of your code. Not fluctuating environment itself. The same if it passes.
Think about it. Builds become repeatable. And it is a huge advantage.
Next. You are running Linux but your folks Windows or Mac? Sure, no problem. You can use your Dockerfile to build an app anywhere. Still isolated and repeatable. Do you want to use CI to build an image? Yep, still works. I hope you are in.
Local build
Backing up to the point, I wanted to build my frontend app using Azure DevOps pipelines and push it to Azure Container Registry. The problem was, that I was using a package from my private Azure Artifacts feed. It's just like the NPM registry but requires you to authenticate.
To do so, Azure Documentation[2] suggests creating two .npmrc files. On project scoped, which stores a reference to the private feed. The second one - user-level - contains the "Personal Access Token" (PAT).
Basically PAT is something like an API key and it would mean to put the PAT token somehow into the container. I didn't like the solution so I started digging in and found an interesting article on how to do it securely[1]. NPM CLI can replace such symbols ${} with an environment variable . It was exactly what I was looking for. I mixed two approaches, the one from the Azure docs and the one from the NPM docs. I ended up with my project scoped .npmrc looking like that:
registry=https://pkgs.dev.azure.com/marcinlovescode/_packaging/marcinlovescode/npm/registry/
always-auth=true
; begin auth token
//pkgs.dev.azure.com/marcinlovescode/_packaging/marcinlovescode/npm/registry/:username=marcinlovescode
//pkgs.dev.azure.com/marcinlovescode/_packaging/ marcinlovescode/npm/registry/:_password=${NPM_TOKEN}
//pkgs.dev.azure.com/marcinlovescode/_packaging/marcinlovescode/npm/registry/:email=npm requires email to be set but doesn't use the value
//pkgs.dev.azure.com/marcinlovescode/_packaging/marcinlovescode/npm/:username=marcinlovescode
//pkgs.dev.azure.com/marcinlovescode/_packaging/marcinlovescode/npm/:_password=${NPM_TOKEN}
//pkgs.dev.azure.com/marcinlovescode/_packaging/marcinlovescode/npm/:email=npm requires email to be set but doesn't use the value
; end auth token
Then, I had to pass my environment variable to the docker build. Take a look at the second and third lines of a Dockerfile.
FROM node:16.14-alpine AS build
ARG NPM_TOKEN=default_value
COPY package.json package-lock.json .npmrc ./
RUN npm i
COPY . .
RUN npm run build
FROM nginx:1.21.6-alpine
COPY --from=build public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
Finally, it's ready to be built. Type
docker build --build-arg NPM_TOKEN=<YOUR TOKEN> .
to make it happen.
CI build - System.AccessToken attempt
As soon as the local build works, the next step is to set up the CI. To authenticate I still needed the token. However, I had in my mind that the Azure pipeline provide an access to System.AccessToken [2]. It's similar to PAT but auto-generated by the build server. To make the local build and CI build be exactly the same I decided to follow the same path and pass it during the pipeline execution.
steps:
- script: |
docker build --build-arg NPM_TOKEN=$(System.AccessToken) -t $IMAGE .
docker push $IMAGE
displayName: 'Build and push to ACR'
Unfortunately, it doesn't work. I don't remember why exactly but there is some kind of a bug. You cannot authenticate to Azure Artifacts feed using that token. I found it after long hours of googling. Somewhere in the deep. Sadly, I lost the link to that issue.
CI build - Azure Task for a rescue
I knew that it must be possible to authenticate somehow because there is a task [3] for that. I mean an Azure Pipeline Task. And it works. I had to figure out how to call the task from the container or simulate its behavior. Haven't wanted to give up, I decided to read the source code of the task. How surprised I was with what I found. Actually, the task does not authenticate. It rewrites the whole .npmrc file, and puts credentials into it!
To make the docker build work, the authenticate task must execute before the docker build. Then, the docker uses rewritten file and is able to authenticate. No more tokens are required.
steps:
- task: npmAuthenticate@0
inputs:
workingFile: .npmrc
- script: |
docker build -t $IMAGE .
docker push $IMAGE
displayName: 'Build and push to ACR'
Summary
I must admit that it was tricky. After several attempts, I managed to make a docker build to pass and I achieved my goal. Every build is isolated and repeatable. What did I learn? The lack of proper documentation and guidance causes a lot of frustration and confusion. Thank god the source code of Azure Tasks is open.
Source code: GitHub
[1] https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow \
[2] https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken \
[3] https://github.com/Microsoft/azure-pipelines-tasks \
[4] Gene Kim, Jez Humble, Patrick Debois, John Willis, (2015, 2016). The DevOps Handbook. ISBN 9781942788003 (2016 ed.) \
[5] Mark Seemann, (2021). Code That Fits in Your Head: Heuristics for Software Engineering. ISBN 9780137464401
[6] Kent Beck, (2004). Extreme Programming Explained: Embrace Change (The XP Series). ISBN 9780321278654
Top comments (0)