Wednesday, April 28, 2021
Java Haiku: Forever
Thursday, April 15, 2021
Creating AWS resources in pytest fixtures with moto
When unit testing functions that access AWS resources, the easiest way is to use the moto library. For instance, I have to test several functions that access an SQS queue, so I proceed this way:
Since I had this create_queue() call at the beginning of most of my test functions, I wanted to make it a fixture. So i tried this way:
Unfortunately, this raised an error in my test functions stating that the queue does not exist. The reason for it is that the decorator @mock_sqs on the fixture would destroy my SQS queue as soon as it would leave the queue() method.
The solution is simple: do not use the mock as a decorator, but trigger it when the fixture is initializing and destroy it when it terminates. That means using a yield within the fixture to return the queue:
Notice that we have to drop the decorator on the test function as well.
Thanks for the answers to this moto issue.
Monday, February 15, 2021
Airflow: Glue Operator looses its region name
The other day, I tried to run an AWS Glue script from our Airflow instance. Nothing fancy, it would just convert a parquet file to CSV between two S3 buckets. Looking for an operator to use, I found that there is indeed a Glue Operator. It looks pretty easy to configure, so I tried it out:
After triggering the DAG, it would fail, telling me that I need to set the region name. Well, I thought I did!
It probably comes from the fact that recent boto3 versions require region to be set, while it was optional in the past. As airflow is open source, I decided to have a look at the code. As it turns out, the bug was not too hard to spot. The Glue Operator creates a Glue Hook, which declares this constructor:
The Hook derives from a base class, AwsBaseHook, that handles the common connection part for all AWS Hooks. The call to the constructor of the super class does not forward the region name. It should probably be called like this:
I opened a bug report. But in the meanwhile, I still needed my code to work. So I found quite an ugly patch. There is probably better, but since I could see that the boto3 session was created in the AwsBaseHook class, and since our Airflow instance is running from an EC2 and I can just inherit its profile, I made this simple workaround:
Hopefully, it won't stay here long...
Friday, December 18, 2020
Group files by folders in Python
I sometimes need to display a list of files coming in this format:
folder1/file1
folder2/file1
folder1/file2 ...
And I want to display it in this format:
folder1
file1
file2
folder2
file1
Here is my code. It uses the groupby function from the itertools library:
Thursday, November 19, 2020
Snake Case in Terraform
How do you convert from camel case to snake case in Terraform? How do you go from "MyProjectName" to "my-project-name"? Here is a simple solution:
Basically, you add an underscore before each capital letters. The strange syntax with the double dollars is a workaround to a strange bug in Terraform, which considers '$1_' as meaning something.
This line works well if you have simple cases like "MyDatabaseName". However, I had to handle some more complex cases, like "ABCProject", or "ProjectABC". In that case, you have to work a bit more.
My solution was to implement two replace functions:
- One for a series of capital letters anywhere in my word. I insert an underscore only if my capital letter is followed by a lower case.
- One for a series of capital letters at the end of my word. I insert an underscore before the first letter of the series.
Friday, June 26, 2020
Work around EC2 Termination Protection in Jenkins Pipeline
resource "aws_instance" "myinstance" { ... disable_api_termination = true }The problem is, when we run our Terraform scripts, and a new instance has to be created instead of an old one (like when we modify user data), it won't let us. You'd have to manually remove the flag. Since we decided that running a Terraform script for deployment means that we really want to be able to terminate our instance, we decide to allow the Jenkins pipeline to remove the flag before running the deployment scripts.
def removeTerminationProtection(instanceName) { echo "Looking for '${instanceName}'" def instanceId = sh (script: "aws ec2 describe-instances --region ${AWS_REGION} --filters Name=tag:Name,Values=${instanceName} --query 'Reservations[0].Instances[0].InstanceId' | xargs echo -n", returnStdout: true) if ('null'.equals(instanceId)) { echo "Instance ${instanceName} not found." } else { echo "Removing termination protection for '${instanceId}'" sh (script: "aws ec2 modify-instance-attribute --instance-id ${instanceId} --region ${AWS_REGION} --disable-api-termination Value=false || exit 1") } }It uses AWS CLI to first find our instance by its name, and then disable termination protection. The way to call it from a pipeline stage is the following:
pipeline { ... stages { ... stage('Remove Termination Protection') { environment { AWS_PROFILE = "${PROFILE_NAME}" } steps { script { removeTerminationProtection("myinstance") } } } ... } }Since we run the AWS CLI, we must have AWS credentials, so we do it using an AWS Profile. We run our Terraform script in a later stage.
Wednesday, April 15, 2020
S3 Multipart Upload of Memory Mapped File in Java
The low level operations of the Multipart Upload expect you to divide your file into parts, each time providing a buffer to the part you want to upload. The best way to go through a big file without reading it all in memory is to use a Memory Mapped File. Here is my code:
private void uploadMultiPartFile(String bucketName, Path path, String key) throws IOException { S3Client s3 = S3Client.builder().region(REGION).build(); CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder() .bucket(bucketName) .key(key) .build(); CreateMultipartUploadResponse response = s3.createMultipartUpload(createMultipartUploadRequest); String uploadId = response.uploadId(); long position = 0; long fileSize = Files.size(path); int part = 1; List<CompletedPart> completedParts = new ArrayList<>(); try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { while (position < fileSize) { long remaining = fileSize - position; int toRead = (int) Math.min(BUFFER_SIZE, remaining); MappedByteBuffer map = channel.map(MapMode.READ_ONLY, position, toRead); UploadPartRequest uploadPartRequest = UploadPartRequest.builder() .bucket(bucketName) .key(key) .uploadId(uploadId) .partNumber(part) .build(); String etag = s3.uploadPart(uploadPartRequest, RequestBody.fromByteBuffer(map)).eTag(); CompletedPart completed = CompletedPart.builder().partNumber(part).eTag(etag).build(); completedParts.add(completed); position += BUFFER_SIZE; part++; } } CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder().parts(completedParts).build(); CompleteMultipartUploadRequest completeMultipartUploadRequest = CompleteMultipartUploadRequest.builder() .bucket(bucketName) .key(key) .uploadId(uploadId) .multipartUpload(completedMultipartUpload) .build(); s3.completeMultipartUpload(completeMultipartUploadRequest); }For my case, I set the BUFFER_SIZE to 100MB. The AWS API states that buffers can be up to 5GB, but I had the bad surprise of noticing that the buffer size parameter in the Java SDK is an int instead of a long, so you cannot use anything above 2GB. When I tried to use 2GB, the process would just hang, so I guess there must be other limitations in the OS.
However, 100MB is OK for me because it allows me to upload files up to 1TB (10000 parts is the maximum allowed). But you can play with this parameter since bigger numbers allow you to upload files faster. Another optimization would be to use a parallel stream to run several uploads in parallel, but I didn't try it.
Another piece of advice if you start playing with Multipart Uploads: if your upload fails at some point, the parts already uploaded are stored in your S3, but you cannot see them, also you pay for them. So do not forget to attach a lifecycle policy for aborted Multipart Uploads.