As @Connor McCarthy said, while waiting for Amazon to come up with a better solution for more permanent keys, in the mean time we'd need to generate the keys on the Jenkins server ourselves somehow.
My solution is to have a periodic job that updates the Jenkins credentials for ECR every 12 hours automatically, using the Groovy API. This is based on this very detailed answer, though I did a few things differently and I had to modify the script.
Steps:
- Make sure your Jenkins master can access the required AWS API. In my setup the Jenkins master is running on EC2 with an IAM role, so I just had to add the permission
ecr:GetAuthorizationToken
to the server role. [update] To get any pushes complete successfully, you'd also need to grant these permissions: ecr:InitiateLayerUpload, ecr:UploadLayerPart, ecr:CompleteLayerUpload, ecr:BatchCheckLayerAvailability, ecr:PutImage
. Amazon has a built-in policy that offers these capabilities, called AmazonEC2ContainerRegistryPowerUser
.
- Make sure that the AWS CLI is installed on the master. In my setup, with the master running in a debian docker container, I've just added this shell build step to the key generation job:
dpkg -l python-pip >/dev/null 2>&1 || sudo apt-get install python-pip -y; pip list 2>/dev/null | grep -q awscli || pip install awscli
- Install the Groovy plugin which allows you to run Groovy script as part of the Jenkins system.
- In the credentials screen, look for your AWS ECR key, click "Advanced" and record its "ID". For this example I'm going to assume it is "12345".
- Create a new job, with a periodic launch of 12 hours, and add a "system Groovy script" build step with the following script:
import jenkins.model.*
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl
def changePassword = { username, new_password ->
def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class,
Jenkins.instance)
def c = creds.findResult { it.username == username ? it : null }
if ( c ) {
println "found credential ${c.id} for username ${c.username}"
def credentials_store = Jenkins.instance.getExtensionList(
'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()
def result = credentials_store.updateCredentials(
com.cloudbees.plugins.credentials.domains.Domain.global(),
c,
new UsernamePasswordCredentialsImpl(c.scope, "12345", c.description, c.username, new_password))
if (result) {
println "password changed for ${username}"
} else {
println "failed to change password for ${username}"
}
} else {
println "could not find credential for ${username}"
}
}
println "calling AWS for docker login"
def prs = "/usr/local/bin/aws --region us-east-1 ecr get-login".execute()
prs.waitFor()
def logintext = prs.text
if (prs.exitValue()) {
println "Got error from aws cli"
throw new Exception()
} else {
def password = logintext.split(" ")[5]
println "Updating password"
changePassword('AWS', password)
}
Please note:
- the use of the hard coded string
"AWS"
as the username for the ECR credentials - this is how ECR works, but if you have multiple credentials with the username "AWS", then you'd need to update the script to locate the credentials based on the description field or something.
- You must use the real ID of your real ECR key in the script, because the API for credentials replaces the credentials object with a new object instead of just updating it, and the binding between the Docker build step and the key is by the ID. If you use the value
null
for the ID (as in the answer I linked before), then a new ID will be created and the setting of the credentials in the docker build step will be lost.
And that's it - the script should be able to run every 12 hours and refresh the ECR credentials, and we can continue to use the Docker plugins.